-
Notifications
You must be signed in to change notification settings - Fork 1
Home
Let's walk through the process of using AspectGenerator to intercept method calls in a C# 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
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
.
The InterceptsLocation
attribute is designed to be used by source generators only.
It takes a file path, line and character position as parameters, so it is not possible to use it directly.
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.8-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();
}
}
}
}
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.
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.
The Aspect
attribute allows you to specify the following methods to control the interception process:
Method | Description | Type |
---|---|---|
OnInit |
Called when the method interception is initialized. | string? |
OnUsing(Async) |
Called to wrap the intercepted method in a using block. |
string? |
OnBeforeCall(Async) |
Called before the intercepted method is called. | string? |
OnCall |
Called instead of the intercepted method. | string? |
OnAfterCall(Async) |
Called after the intercepted method is called. | string? |
OnCatch(Async) |
Called when the intercepted method throws an exception. | string? |
OnFinally(Async) |
Called when the intercepted method exits. | string? |
InterceptMethods |
Specifies which methods to intercept. | string[]? |
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;
}
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.
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);
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);
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.
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.
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.
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.
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.
Tipically, you do not need to specify the InterceptMethods
property.
AG will intercept all methods decorated with your aspects.
However, you can specify the InterceptMethods
property to intercept methods explicitly.
The InterceptMethods
property should contain the array of the method names to intercept.
[Aspect(
OnAfterCall = nameof(OnAfterCall),
InterceptMethods = new[]
{
"string.Substring(int)"
}
)]
sealed class InterceptMethodsAttribute
{
public static void OnAfterCall(InterceptInfo<string> info)
{
info.ReturnValue += " + InterceptMethods aspect.";
}
}
Now the Substring
method will be intercepted across the project and will return some unexpected results.
Warning
Disclaimer: this stunt is performed by a trained professionals. Please do not try this on production.
In addition, you can control the code generation process by specifying the following properties of the Aspect
attribute:
Property | Description | Type |
---|---|---|
PassArguments |
Specifies whether to pass the intercepted method arguments to the aspect. | bool |
UseInterceptType |
Specifies whether to use the InterceptType property of the InterceptInfo object. |
bool |
UseInterceptData |
Specifies whether to use struct InterceptData or class InterceptInfo to pass information about the intercepted method. |
bool |
The PassArguments
property specifies whether to pass the intercepted method arguments to the aspect.
If you set the PassArguments
property to true
, the InterceptInfo
object will contain the MethodArguments
property with the array of the intercepted method arguments.
public static string ArgumentsInMethod_Interceptor(string s, int i, in bool b, ref int? _)
{
var __args__ = new object?[] { s, i, b, _ }; // Prepare arguments
// Aspects.ArgumentsAttribute
//
var __info__0 = new AspectGenerator.InterceptInfo<string>
{
MemberInfo = ArgumentsInMethod_Interceptor_MemberInfo,
AspectType = typeof(Aspects.ArgumentsAttribute),
AspectArguments = ArgumentsInMethod_Interceptor_AspectArguments_0,
MethodArguments = __args__, // Pass arguments
};
__info__0.ReturnValue = AspectGenerator.Tests.UnitTests.ArgumentsInMethod(s, i, in b, ref _);
Aspects.ArgumentsAttribute.OnAfterCall(__info__0);
return __info__0.ReturnValue;
}
The UseInterceptType
property specifies whether to use the InterceptType
property of the InterceptInfo
object. If you set the UseInterceptType
property to true
, the InterceptInfo
object will contain the InterceptType
property with the type of the interception.
public static string ExtensionMethod_Interceptor(this AspectGenerator.Tests.UnitTests __this__, int value)
{
// Aspects.TestAspectAttribute
//
var __info__0 = new AspectGenerator.InterceptInfo<string>
{
MemberInfo = ExtensionMethod_Interceptor_MemberInfo,
AspectType = typeof(Aspects.TestAspectAttribute),
AspectArguments = ExtensionMethod_Interceptor_AspectArguments_0,
};
__info__0.ReturnValue = AspectGenerator.Tests.TestCodeExtensions.ExtensionMethod(__this__, value);
__info__0.InterceptType = AspectGenerator.InterceptType.OnAfterCall; // Use InterceptType
Aspects.TestAspectAttribute.OnAfterCall(__info__0);
return __info__0.ReturnValue;
}
AG can provide information about the intercepted method in two ways: by using struct InterceptData<T>
or by using class InterceptInfo<T>
.
These data structures are similar except:
-
InterceptData<T>
is astruct
andInterceptInfo<T>
is aclass
. -
InterceptData<T>
does not have thePreviousInfo
property. -
InterceptData<T>
does not have non-generic version.
Also you have to use ref
keyword to specify InterceptData<T>
as a parameter of the aspect method.
[Aspect(
OnUsing = nameof(OnUsing),
OnUsingAsync = nameof(OnUsingAsync),
UseInterceptData = true
)]
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
sealed class UsingAttribute : Attribute
{
public static IDisposable? OnUsing<T>(ref InterceptData<T> info)
{
return null;
}
public static IAsyncDisposable? OnUsingAsync<T>(ref InterceptData<T> info)
{
return null;
}
}
By default AG uses class InterceptInfo<T>
to pass information about the intercepted method. If you set the UseInterceptData
property to true
, AG will use struct InterceptData<T>
instead.
This feature can be used to optimize the performance of the garbage collector.
If you use struct InterceptData<T>
, the InterceptData<T>
object will be allocated on the stack and will be collected automatically when the method exits.
If you use class InterceptInfo<T>
, the InterceptInfo<T>
object will be allocated on the heap and will be collected by the garbage collector.
By default, AG generates your aspects in the Interceptors
namespace.
You can change this behavior by specifying the AG_InterceptorsNamespace
property in your project file:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Define your own namespace where AG will generate your aspects. -->
<AG_InterceptorsNamespace>MyInterceptors</AG_InterceptorsNamespace>
</PropertyGroup>
<ItemGroup>
<!-- Add this line to make "AG_InterceptorsNamespace" property visible to compiler and AG. -->
<CompilerVisibleProperty Include="AG_InterceptorsNamespace" />
</ItemGroup>
</Project>
Source generators can generate code only for the project they are added to. However, you can use AG to intercept methods defined in other projects of your solution. It can read parameters of your aspect attributes and generate code for them. But you need to define the same aspects in your current project as well. You do not need to copy/paste the code of your aspects. Just add the file with your aspects to the current project as link.
You can create an aspect library and use it in your projects. Just create a new project and add your aspects to it. Then add the following lines to your aspect library project file:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Add this line to make AG API public and visible to other projects. -->
<DefineConstants>$(DefineConstants);AG_PUBLIC_API</DefineConstants>
<InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);AspectGenerator</InterceptorsPreviewNamespaces>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AspectGenerator" Version="0.0.8-preview" />
</ItemGroup>
</Project>
Reference your aspect library from your projects and add the following lines to your project file:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<!-- Add this line to disable AG API generation. -->
<DefineConstants>$(DefineConstants);AG_NOT_GENERATE_API</DefineConstants>
<InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);AspectGenerator</InterceptorsPreviewNamespaces>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\AspectLibrary\AspectLibrary.csproj" />
<PackageReference Include="AspectGenerator" Version="0.0.8-preview" />
</ItemGroup>
</Project>
Here are some funny stuff generated by GitHub Copilot. You do not need to follow these recommendations, they are not going to work anyway.
Tip
If you want to intercept all methods in the project, you can specify the InterceptMethods
property with the *
value.
Oh, yeah, it's true, once we start intercepting, we will never stop! Of course, we really need to intercept everything in this infinite universe!