Skip to content
Igor Tkachev edited this page Dec 12, 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 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:

<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>

        <!-- Add these lines if you want to see generated code. -->
        <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
        <CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GeneratedFiles</CompilerGeneratedFilesOutputPath>
    </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 support classes. Fully generated code can be found here.

Now you can define your own aspects which are just attributes decorated with the 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:

// <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,
            };

            Aspects.InterceptAttribute.OnBeforeCall(__info__0);

            if (__info__0.InterceptResult != AspectGenerator.InterceptResult.Return)
            {
                A.InterceptableMethod();
            }
        }
    }
}

Creating your own aspect

InterceptInfo<T>

The InterceptInfo class contains information about the intercepted method. It has the following properties:

InterceptInfo Properties Description Type
IntercepType The type of the interception. See below for more information. InterceptType
InterceptResult The result of the interception. See below for more information. InterceptResult
Exception The exception thrown by the intercepted method. Exception?
MemberInfo The MethodInfo object of the intercepted method. MemberInfo
MethodArguments The array of the intercepted method arguments. object?[]?
AspectType The Type object of the aspect. Type
AspectArguments The dictionary of aspect arguments. Dictionary<string,object?>
PreviousInfo The previous InterceptInfo object if more than one aspect is specified. InterceptInfo?
Tag The tag object. You can use it to pass information between aspect methods. object?

The InterceptInfo<T> class is a generic version of the InterceptInfo class. It has the following additional properties:

InterceptInfo<T> Description Type
ReturnValue The return value of the intercepted method. T

Note

If you want to change the return value of the intercepted method, you should use the generic version of the InterceptInfo class.

Aspect attribute

AG generates the Aspect attribute and support classes for you.

Tip

If you want to see the generated code, add the following lines to your project file:

<PropertyGroup>
    <!-- Add these lines if you want to see generated code. -->
    <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
    <CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GeneratedFiles</CompilerGeneratedFilesOutputPath>
</PropertyGroup>

Now you can find the generated code in obj/GeneratedFiles/AspectGenerator/AspectGenerator.AspectSourceGenerator/AspectAttribute.g.cs file.

Controlling the interception process

The Aspect attribute allows you to specify the following methods to control the interception process:

Method Description
OnInit Called when the method interception is initialized.
OnUsing(Async) Called to wrap the intercepted method in a using block.
OnBeforeCall(Async) Called before the intercepted method is called.
OnCall Called instead of the intercepted method.
OnAfterCall(Async) Called after the intercepted method is called.
OnCatch(Async) Called when the intercepted method throws an exception.
OnFinally(Async) Called when the intercepted method exits.

To specify a method, just set the corresponding property of the Aspect attribute:

[Aspect(
    OnBeforeCall = nameof(OnBeforeCall)
    )]
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
class InterceptAttribute : Attribute
{
    public static void OnBeforeCall(InterceptInfo info)
    {
        Console.WriteLine("aspected");
    }
}

You can specify one or more methods. If you specify more than one method, they will be called in the the folowing order:

public static int InterceptableMethod_Interceptor()
{
    // Aspects.MyAspectAttribute
    //
    var __info__0 = new AspectGenerator.InterceptInfo<int>
    {
        MemberInfo      = InterceptableMethod_Interceptor_MemberInfo,
        AspectType      = typeof(Aspects.MyAspectAttribute),
        AspectArguments = InterceptableMethod_Interceptor_AspectArguments_0,
    };

    __info__0 = Aspects.MyAspectAttribute.OnInit(__info__0);                        // OnInit

    using (Aspects.MyAspectAttribute.OnUsing(__info__0))                            // OnUsing
    {
        try
        {
            Aspects.MyAspectAttribute.OnBeforeCall(__info__0);                      // OnBeforeCall

            if (__info__0.InterceptResult != AspectGenerator.InterceptResult.Return)
            {
                __info__0.ReturnValue = A.InterceptableMethod();

                Aspects.MyAspectAttribute.OnAfterCall(__info__0);                   // OnAfterCall
            }
        }
        catch (Exception __ex__)
        {
            __info__0.Exception       = __ex__;
            __info__0.InterceptResult = AspectGenerator.InterceptResult.ReThrow;

            Aspects.MyAspectAttribute.OnCatch(__info__0);                           // OnCatch

            if (__info__0.InterceptResult == AspectGenerator.InterceptResult.ReThrow)
                throw;
        }
        finally
        {
            Aspects.MyAspectAttribute.OnFinally(__info__0);                         // OnFinally
        }
    }

    return __info__0.ReturnValue;
}

Asynchronous methods

You can specify asynchronous methods as well:

public static async System.Threading.Tasks.Task<int> InterceptableMethodAsync_Interceptor()
{
    // Aspects.MyAspectAttribute
    //
    var __info__0 = new AspectGenerator.InterceptInfo<int>
    {
        MemberInfo      = InterceptableMethodAsync_Interceptor_MemberInfo,
        AspectType      = typeof(Aspects.MyAspectAttribute),
        AspectArguments = InterceptableMethodAsync_Interceptor_AspectArguments_0,
    };

    __info__0 = Aspects.MyAspectAttribute.OnInit(__info__0);

    await using (Aspects.MyAspectAttribute.OnUsingAsync(__info__0))
    {
        try
        {
            await Aspects.MyAspectAttribute.OnBeforeCallAsync(__info__0);

            if (__info__0.InterceptResult != AspectGenerator.InterceptResult.Return)
            {
                __info__0.ReturnValue = await A.InterceptableMethodAsync();

                await Aspects.MyAspectAttribute.OnAfterCallAsync(__info__0);
            }
        }
        catch (Exception __ex__)
        {
            __info__0.Exception       = __ex__;
            __info__0.InterceptResult = AspectGenerator.InterceptResult.ReThrow;

            await Aspects.MyAspectAttribute.OnCatchAsync(__info__0);

            if (__info__0.InterceptResult == AspectGenerator.InterceptResult.ReThrow)
                throw;
        }
        finally
        {
            await Aspects.MyAspectAttribute.OnFinallyAsync(__info__0);
        }
    }

    return __info__0.ReturnValue;
}

If you specify regular method and you do not specify asynchronous methods, AG will generate call to regular methods instead.

OnInit

The OnInit method is called when the method interception is initialized. You can use it to initialize or recreate provided InterceptorInfo object. As you can see from the generated code, the OnInit method is called as a static method of the aspect class and should have the following signature:

public static InterceptInfo<T> OnInit<T>(InterceptInfo<T> info);

OnUsing(Async)

The OnUsing method is called to wrap the intercepted method in a using block. The OnUsing method is called as a static method of the aspect class and should have the following signatures:

public static IDisposable? OnUsing(InterceptInfo info);
// or
public static IDisposable? OnUsing<T>(InterceptInfo<T> info);

Also you can define more than one generic version of the OnUsing method for different types:

public static IDisposable? OnUsing<T>(InterceptInfo<T> info);

public static IDisposable? OnUsing<int>(InterceptInfo<int> info);

public static IDisposable? OnUsing<string>(InterceptInfo<string> info);

In this case, the compiler will choose the most specific version of the OnUsing method.

Asynchronous version of the OnUsing method has the following signatures:

public static IAsyncDisposable? OnUsingAsync<T>(InterceptInfo<T> info);

OnBeforeCall(Async)

The OnBeforeCall method is called before the intercepted method is called. The OnBeforeCall method is called as a static method of the aspect class and should have the following signatures:

public static void OnBeforeCall(InterceptInfo info);

public static void OnBeforeCall<T>(InterceptInfo<T> info);

public static Task OnBeforeCallAsync(InterceptInfo info)

public static Task OnBeforeCallAsync<T>(InterceptInfo<T> info)

You can also define specific generic versions of the OnBeforeCall method for different types.

Note

This method can change the InterceptResult property of the InterceptInfo object to control the interception process. If you set the InterceptResult property to InterceptResult.Return, the intercepted method will not be called and the OnAfterCall method will not be called either.

OnCall

The OnCall method is called instead of the intercepted method. The OnCall method is called as a static method of the aspect class and should have the signatures similar to the intercepted method. If you do not specify the OnCall method, the intercepted method will be called instead.

OnAfterCall(Async)

The OnAfterCall method is called after the intercepted method is called. The OnAfterCall method is called as a static method of the aspect class and should have the following signatures:

public static void OnAfterCall(InterceptInfo info);

public static void OnAfterCall<T>(InterceptInfo<T> info);

public static Task OnAfterCallAsync(InterceptInfo info)

public static Task OnAfterCallAsync<T>(InterceptInfo<T> info)

You can also define specific generic versions of the OnAfterCall method for different types.

OnCatch(Async)

The OnCatch method is called when the intercepted method throws an exception. The OnCatch method is called as a static method of the aspect class and should have the following signatures:

public static void OnCatch(InterceptInfo info);

public static void OnCatch<T>(InterceptInfo<T> info);

public static Task OnCatchAsync(InterceptInfo info)

public static Task OnCatchAsync<T>(InterceptInfo<T> info)

You can also define specific generic versions of the OnCatch method for different types.

Note

This method can change the InterceptResult property of the InterceptInfo object to control the interception process. If you set the InterceptResult property to InterceptResult.IgnoreThrow, the exception will not be rethrown.

OnFinally(Async)

The OnFinally method is called when the intercepted method exits. The OnFinally method is called as a static method of the aspect class and should have the following signatures:

public static void OnFinally(InterceptInfo info);

public static void OnFinally<T>(InterceptInfo<T> info);

public static Task OnFinallyAsync(InterceptInfo info)

public static Task OnFinallyAsync<T>(InterceptInfo<T> info)

You can also define specific generic versions of the OnFinally method for different types.

Clone this wiki locally