Skip to content
Igor Tkachev edited this page Dec 8, 2023 · 27 revisions

How it works

Let's walk through the process of using AspectGenerator to intercept method calls in a C# project.

Create a new project

AspectTest.csproj:

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net8.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
    </PropertyGroup>
</Project>

Program.cs:

A.InterceptableMethod();

static class A
{
    public static void InterceptableMethod()
    {
        // This method just prints "interceptable" to console.
        //
        Console.WriteLine("interceptable");
    }
}

Run the program it and see the output:

interceptable

Use InterceptsLocation attribute

Now let's add InterceptsLocation attribute to intercept the call to InterceptableMethod.

Modified project file:

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net8.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>

        <-- Add this line -->
        <InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);Interceptors</InterceptorsPreviewNamespaces>
    </PropertyGroup>
</Project>

Program.cs:

A.InterceptableMethod();

static class A
{
    public static void InterceptableMethod()
    {
        Console.WriteLine("interceptable");
    }
}

namespace Interceptors
{
    using System.Runtime.CompilerServices;

    class B
    {
        // This method will be called instead of `InterceptableMethod`.
        // 'InterceptsLocation' attribute tells compiler to replace call to `InterceptableMethod`
        // with call to `InterceptorMethod`.
        //
        [InterceptsLocation(@"P:\Test\AspectTest\Program.cs", line: 1, character: 3)]
        public static void InterceptorMethod()
        {
            Console.WriteLine("interceptor");
        }
    }
}

// For now we have to define `InterceptsLocation` attribute ourselves.
//
namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
    public sealed class InterceptsLocationAttribute(string filePath, int line, int character) : Attribute
    {
    }
}

Now, when you run the program, you should see a different output:

interceptor

Note

When the compiler encounters the InterceptsLocation attribute, it replaces the call to InterceptableMethod with a call to InterceptorMethod.

Use AspectGenerator

The InterceptsLocation attribute is design to be used by source generators only. It requires an absolute path to file, line and character position, , making direct use impractical.

AspectGenerator (AG) is one such source generators.

Let's modify our project to utilize AG.

Modified project file:

```xml
<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net8.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>

        <!-- Add this line -->
        <InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);AspectGenerator</InterceptorsPreviewNamespaces>
    </PropertyGroup>

    <ItemGroup>
        <!-- Add AG package. -->
        <PackageReference Include="AspectGenerator" Version="0.0.5-preview" />
    </ItemGroup>
</Project>

When you add AG package to your project, it generates the Aspect attribute and supported environment. Details can be found here.

Now you can define your own aspects which are just attributes decorated with Aspect attribute.

A.InterceptableMethod();

static class A
{
    // Use your aspect.
    //
    [Aspects.Intercept]
    public static void InterceptableMethod()
    {
        Console.WriteLine("interceptable");
    }
}

namespace Aspects
{
    using AspectGenerator;

    // Define your own aspect.
    //
    [Aspect(
        OnBeforeCall = nameof(OnBeforeCall)
        )]
    class InterceptAttribute : Attribute
    {
        public static void OnBeforeCall(InterceptInfo info)
        {
            Console.WriteLine("aspected");
            info.InterceptResult = InterceptResult.Return;
        }
    }
}

Now you should see the following output:

aspected

AG generates interceptor methods decorated with InterceptsLocation attribute for each method decorated with Intercept attribute. You can find the generated code in obj/GeneratedFiles/AspectGenerator/AspectGenerator.AspectSourceGenerator/Interceptors.g.cs file.

Interceptors.g.cs:

// <auto-generated/>
#pragma warning disable
#nullable enable

using System;

using SR  = System.Reflection;
using SLE = System.Linq.Expressions;
using SCG = System.Collections.Generic;

namespace AspectGenerator
{
    using AspectGenerator = AspectGenerator;

    static partial class Interceptors
    {
        static SR.MethodInfo GetMethodInfo(SLE.Expression expr)
        {
            return expr switch
            {
                SLE.MethodCallExpression mc => mc.Method,
                _                           => throw new InvalidOperationException()
            };
        }

        static SR.MethodInfo MethodOf<T>(SLE.Expression<Func<T>> func) => GetMethodInfo(func.Body);
        static SR.MethodInfo MethodOf   (SLE.Expression<Action>  func) => GetMethodInfo(func.Body);

        static SR. MemberInfo                 InterceptableMethod_Interceptor_MemberInfo        = MethodOf(() => A.InterceptableMethod());
        static SCG.Dictionary<string,object?> InterceptableMethod_Interceptor_AspectArguments_0 = new()
        {
        };
        //
        /// <summary>
        /// Intercepts A.InterceptableMethod().
        /// </summary>
        //
        // Intercepts A.InterceptableMethod().
        [System.Runtime.CompilerServices.InterceptsLocation(@"P:\Test\AspectTest\Program.cs", line: 1, character: 3)]
        //
        [System.Runtime.CompilerServices.CompilerGenerated]
        //[System.Diagnostics.DebuggerStepThrough]
        public static void InterceptableMethod_Interceptor()
        {
            // Aspects.InterceptAttribute
            //
            var __info__0 = new AspectGenerator.InterceptInfo<AspectGenerator.Void>
            {
                MemberInfo      = InterceptableMethod_Interceptor_MemberInfo,
                AspectType      = typeof(Aspects.InterceptAttribute),
                AspectArguments = InterceptableMethod_Interceptor_AspectArguments_0,
            };

            __info__0.InterceptType = AspectGenerator.InterceptType.OnBeforeCall;
            Aspects.InterceptAttribute.OnBeforeCall(__info__0);

            if (__info__0.InterceptResult != AspectGenerator.InterceptResult.Return)
            {
                A.InterceptableMethod();
            }
        }
    }
}
Clone this wiki locally