Compile time weaver for AOP implementation
(Note: This post is for NuGet package v 2.3.0, please reference to document for newer version 3.3.3 for NetStandard platform.)
1. Introduce
CompileTimeWeaver.Fody completes IL weaving when VS.net build your project without any extra tool to install. Different from runtime interception with dynamic proxy, this tool rewrites assembly at VisualStudio.Net build time, so that your code get these achievements that dynamic proxy based enhancement cannot give:
- Much better performance
- Directly instantiate your intercepted classes with C# "new" keyword, no proxy classes exist at all.
- Intercept virtual methods/properties
- Intercept static methods/properties
- Intercept extension methods
- Intercept constructors
- Intercept async methods
- Support source code debug
CompileTimeWeaver.Fody supports two programming models: advice based programming model (since version 2) and decorator based programming model (since version 1). Advice based programming model has these superiorities comparing to the old decorator based monogramming model in version 1:
- Easy to intercept async method without internal blocking
- Simple to control the flow and add "before", "after", "around" and "exception" advises
- Allow reference type, value type, generic type, ref, and out parameters
2. Advice Based Programming Model
2.1. Step-by-step Instruction
Step 1) Create a C# project with VisualStudio.Net
Step 2) Install nuget package CompileTimeWeaver.Fody
Step 2) Install nuget package CompileTimeWeaver.Fody
Download and install NuGet package from https://www.nuget.org/packages/CompileTimeWeaver.Fody, you will see a FodyWeavers.xml file is added into project. The FodyWeavers.xml file content should have "CompileTimeWeaver" node as below, add it manually if the CompileTimeWeaver node is missing.
<?xml version="1.0" encoding="utf-8"?>
<Weavers>
<CompileTimeWeaver />
</Weavers>
Also, you will see CompileTimeWeaverInterfaces is added as a reference assembly.
Step 3) Add your advice class as below
using CompileTimeWeaverInterface;
public class MyAdvice : AdviceAttribute
{
public override object Advise(IInvocation invocation)
{
// do something before target method is Called
// ...
Trace.WriteLine("Entering " + invocation.Method.Name);
try
{
return invocation.Proceed(); // call the next advice in the "chain" of advice pipeline, or call target method
}
catch (Exception e)
{
// do something when target method throws exception
// ...
Trace.WriteLine("MyAdvice catches an exception: " + e.Message);
throw;
}
finally
{
// do something after target method is Called
// ...
Trace.WriteLine("Leaving " + invocation.Method.Name);
}
}
public override async Task<object> AdviseAsync(IInvocation invocation)
{
// do something before target method is Called
// ...
Trace.WriteLine("Entering async " + invocation.Method.Name);
try
{
return await invocation.ProceedAsync(); // asynchroniously call the next advice in the "chain" of advice pipeline, or call target method
}
catch (Exception e)
{
// do something when target method throws exception
// ...
Trace.WriteLine("MyAdvice catches an exception: " + e.Message);
throw;
}
finally
{
// do something after target method is Called
// ...
Trace.WriteLine("Leaving async " + invocation.Method.Name);
}
}
}
Step 4) Add [MyAdvice] to any of your class on class level or method level.
For example:
[MyAdvice]
public class MyClass
{
public int Add(int x, int y)
{
return x + y;
}
public Task<int> AddAsync(int x, int y)
{
await Task.Delay(1000);
return x + y;
}
}
That is it. You don't need dynamic proxy such as Castle DynamicProxy, directly use you class as usual in C#, for example:
var obj = new MyClass();
int z = obj.Add(1, 2);
z = await obj.AddAsync(1,2);
Debug your code, you will see your Add() method and AddAsync() method are magically intercepted, and the output is as below:
Entering .ctor...
Leaving .ctor...
Entering Add...
Leaving Add...
Entering AddAsync...
Leaving AddAsync...
[MyAdvice]
public class MyClass
{
public int Add(int x, int y)
{
return x + y;
}
public Task<int> AddAsync(int x, int y)
{
await Task.Delay(1000);
return x + y;
}
}
var obj = new MyClass();
int z = obj.Add(1, 2);
Entering .ctor...
Leaving .ctor...
Entering Add...
Leaving Add...
Entering AddAsync...
Leaving AddAsync...
2.2. Advice class specification
-
Inherits AdviceAttribute class, or
-
Inherits Attribute class and implements IAdvice interface
- Inherits AdviceAttribute class, or
- Inherits Attribute class and implements IAdvice interface
2.3. Rules
-
When an advice attribute is applied to a class, it is identical to this advice attribute is added on each method.
-
Class advices appear before all method advices
-
When multiple advices are applied to a method, the advices are invoked in the chain of a pipe line
-
Use these named property to disable IL weaving on constructors, auto properites, getters, setters and static, for example:
[MyAdvice(ExcludeConstructors=true)]
[MyAdvice(ExcludeAutoProperties=true)][MyAdvice(ExcludeGetters=true)][MyAdvice(ExcludeSetters=true)][MyAdvice(ExcludeStatic=true)]
- The Order property of advice instance is automatically assigned the sequence number of its appearance in each type group, for example:
[AdviceA] //Order is 0
[AdviceB] //Order is 0
[AdviceA] //Order is 1
public class MyClass
{
[AdviceB] //Order is 1 [AdviceA] //Order is 2 [AdviceB] //Order is 2 [AdviceB] //Order is 3 public int Add(int x, int y)
{
return x+y;
}
}
- With the value of the Order property, you know the invocation sequence of the advices in the same type group.
- When an advice attribute is applied to a class, it is identical to this advice attribute is added on each method.
- Class advices appear before all method advices
- When multiple advices are applied to a method, the advices are invoked in the chain of a pipe line
- Use these named property to disable IL weaving on constructors, auto properites, getters, setters and static, for example:
[MyAdvice(ExcludeConstructors=true)]
[MyAdvice(ExcludeAutoProperties=true)][MyAdvice(ExcludeGetters=true)][MyAdvice(ExcludeSetters=true)][MyAdvice(ExcludeStatic=true)]
- The Order property of advice instance is automatically assigned the sequence number of its appearance in each type group, for example:
[AdviceA] //Order is 0
[AdviceB] //Order is 0
[AdviceA] //Order is 1
public class MyClass
{
[AdviceB] //Order is 1 [AdviceA] //Order is 2 [AdviceB] //Order is 2 [AdviceB] //Order is 3 public int Add(int x, int y)
{
return x+y;
}
}
- With the value of the Order property, you know the invocation sequence of the advices in the same type group.
3. Decorator Based Programming Model
This programming model is there since version 1.0, but it is not recommended, advice based programming model is better than it. Below is given the code of this model.
3.1. You Code
3.1. You Code
Add your interceptor class as below:
using CompileTimeWeaverInterface;
public class InterceptorAttribute : DecoratorAttribute
{
/// <summary>
/// Called before calling the next interceptor or target. If returns false, the next interceptor or target will not be called.
/// </summary>
/// <param name="instance">Instance of target class, null when the decorated method is static or a constructor</param>
/// <param name="method">Intercepted method</param>
/// <param name="args">Parameters of intercepted method</param>
/// <returns></returns>
public override bool OnEntry(object instance, MethodBase method, object[] args)
{
Trace.WriteLine(String.Format("InterceptorAttribute.OnEntry"));
return true;
}
/// <summary>
/// Called after the call to the next intercepter or target completed without exception.
/// </summary>
/// <param name="instance">Instance of target class, null when the intercepted method is static or a constructor</param>
/// <param name="method">Intercepted method</param>
/// <param name="args">Parameters of intercepted method</param>
/// <param name="returnValue">Return value of intercepted method. null when the intercepted method return type is void.</param>
/// <returns>New return value</returns>
public override object OnExit(object instance, MethodBase method, object[] args, object returnValue)
{
Trace.WriteLine("InterceptorAttribute.OnExit");
// you can change returnValue and return a new value here
// ...
return returnValue;
}
/// <summary>
/// Called if the next interceptor or target throws. Rethrow if this method return true.
/// </summary>
/// <param name="instance">Instance of target class, null when the decorated method is static or a constructor</param>
/// <param name="method">Intercepted method</param>
/// <param name="args">Parameters of intercepted method</param>
/// <param name="exception">The exception threw from intercepted method</param>
/// <returns>Boolean, true to rethrow</returns>
public override bool OnException(object instance, MethodBase method, object[] args, Exception exception)
{
Trace.WriteLine("InterceptorAttribute.OnException");
return true;
}
// OnTaskContinuation is optional
public override void OnTaskContinuation(Task task)
{
Trace.WriteLine("InterceptorAttribute.OnTaskContinuation");
task.Wait();
}
}
Decorate any of your class with [Interceptor] on class level or method level.
For example:
public class MyClass
{
[Interceptor]
public int Add(int x, int y)
{
return x + y;
}
}
That is it. Directly use you decorated class as usual in C#, for example:
var obj = new MyClass();
int z = obj.Add(1, 2);
Debug your code, you will see your Add() method is intercepted.
3.2. Interceptor class specification
- Inherits DecoratorAttribute class, or
- Inherits Attribute class and implements IDecorator interface
3.3. Rules
- When a decorator is applied to a class, it is identical to this decorator is added on each method.
- Class decorators appears before all method decorators
- In case of exception in async method you "OnException" will not be called, OnTaskContinuation will be called instead.
- When multiple decorators are applied to a method, the decorators are invoked as a pip line, the OnEntry of the first decorator is called first and its OnExit is called at last.
- Use these named property to disable IL weaving on constructors, auto properites, getters, setters and static, for example:
[Interceptor(ExcludeConstructors=true)] [Interceptor(ExcludeAutoProperties=true)] [Interceptor(ExcludeGetters=true)] [Interceptor(ExcludeSetters=true)] [Interceptor(ExcludeStatic=true)]
- The Order property of interceptor instance is automatically assigned the sequence number of its appearance in each type group, for example:
[InterceptorA] //Order is 0 [InterceptorB] //Order is 0 [InterceptorA] //Order is 1 public class MyClass { [InterceptorB] //Order is 1 [InterceptorA] //Order is 2 [InterceptorB] //Order is 2 [InterceptorB] //Order is 3 public int Add(int x, int y) { return x+y; } }
With the value of the Order property, you know the invocation sequence of the interceptors in the same type group.
3.4. IL Weaving
Decompile the vs.net generated assembly containing MyClass class, you see MyClass is changed into it below:
public class MyClass
{
[Interceptor]
public int Add(int x, int y)
{
MethodBase methodBase = MethodBase.GetMethodFromHandle(__methodref (Sample.Add), __typeref (Sample));
object[] args = new object[2] {(object) x, (object) y};
var attributes = typeof(MyClass).GetCustomAttributes(typeof(AdviceAttribute), true)
.Concat(method.GetCustomAttributes(typeof(AdviceAttribute), false))
.Cast<AdviceAttribute>()
.ToArray();
InterceptorAttribute interceptorAttribute = attributes[0];
interceptorAttribute.Order = 0;
int num;
if (interceptorAttribute.OnEntry((object) this, methodFromHandle, args))
{
try
{
Trace.WriteLine("Your Code");
num = x + y;
num = (int) interceptorAttribute.OnExit((object) this, methodFromHandle, args, (object) num);
}
catch (Exception ex)
{
if (interceptorAttribute.OnException((object) this, methodFromHandle, args, ex))
throw;
}
}
return num;
}
}
}
NOTE: Keyword "this" is replaced by "null" when the decorated method is static.
3.5. Release mode optimization
The tool is smart enough to weave interceptor into IL only when it is necessary. For example, if you change OnExit to return original return value, change OnException to return true, they are not weaved into IL in release mode.
Your Code:
public class InterceptorAttribute : DecoratorAttribute
{
public override bool OnEntry(object instance, MethodBase method, object[] args)
{
Trace.WriteLine(String.Format("InterceptorAttribute.OnEntry"));
return true;
}
public override object OnExit(object instance, MethodBase method, object[] args, object returnValue)
{
return returnValue;
}
public override bool OnException(object instance, MethodBase method, object[] args, Exception exception)
{
return true;
}
}
You see this if you decompile the release mode assembly generated by VS.net, only OnEntry method is weaved, it is unnecessary to weave the other methods:
public class MyClass
{
[Interceptor]
public int Add(int x, int y)
{
MethodBase methodFromHandle = MethodBase.GetMethodFromHandle(__methodref (Sample.Add), __typeref (Sample));
object[] args = new object[2] {(object) x, (object) y};
var attributes = typeof(MyClass).GetCustomAttributes(typeof(AdviceAttribute), true)
.Concat(method.GetCustomAttributes(typeof(AdviceAttribute), false))
.Cast<AdviceAttribute>()
.ToArray();
InterceptorAttribute interceptorAttribute = attributes[0];
interceptorAttribute.Order = 0;
int num;
if (interceptorAttribute.OnEntry((object) this, methodFromHandle, args))
{
Trace.WriteLine("Your Code");
num = x + y;
}
return num;
}
}
}
3.6. Intercept async method
OnTaskContinuation method is useful to intercept async method. For example, you connect to database in OnEntry and disconnect it in OnExit. When intercepted method is an async method to access database, OnExit may be called to close database connection before all the works in the method complete. The solution of it is to block wait for the continuation task (that code below await) completes by adding OnTaskContinuation() into interceptor (See example code below). If you don't intercept target method without blocking, please take advantage of advice based programming model in version 2.0+
public virtual void OnTaskContinuation(Task task)
{
task.Wait();
}
4. Predefined Aspects
The nuget package predefined a few useful AOP aspects (I still name them advises in source code for historically reason, but it is more accurate to call them aspects) to streamline the coding in .net languages. You can simply apply the aspects on any type or method with corresponding attribute annotations.
The nuget package predefined a few useful AOP aspects (I still name them advises in source code for historically reason, but it is more accurate to call them aspects) to streamline the coding in .net languages. You can simply apply the aspects on any type or method with corresponding attribute annotations.
4.1. Logging Aspect
The code below automatically logs method name and parameters through nlog or log4net, depending if nlog or log4net is referenced by application.
[Logging]
public void Test(int x, int y)
{
…
}
The code below automatically logs all methods and properties except Dispose() method.
[Logging]
public class ManageUsersRepository : IManageUsersRepository
{
…
…
[Logging(IgnoreLogging=true)]
public void Dispose()
{
…
}
}
The code below automatically logs method name and parameters through nlog or log4net, depending if nlog or log4net is referenced by application.
[Logging] public void Test(int x, int y) { … }
The code below automatically logs all methods and properties except Dispose() method.
[Logging] public class ManageUsersRepository : IManageUsersRepository { … … [Logging(IgnoreLogging=true)] public void Dispose() { … } }
4.2. Exception Handling Aspect
The code below demonstrates how to use exception handler class to handle ApplicationException thrown by MyClass class:
[ExceptionHandler(typeof(ApplicationException), typeof(ApplicationExceptionHandler))]
internal class MyClass
{
public void Hello()
{
throw new ApplicationException();
}
}
The ApplicationExceptionHandler class must implement IExceptionHandler interface:
internal class ApplicationExceptionHandler : IExceptionHandler
{
public bool HandleException(Exception e)
{
Trace.WriteLine(e.GetType()+" is caught");
return false; //return true to rethrow
}
}
The code below demonstrates how to use exception handler class to handle ApplicationException thrown by MyClass class:
[ExceptionHandler(typeof(ApplicationException), typeof(ApplicationExceptionHandler))]
internal class MyClass{
public void Hello()
{
throw new ApplicationException();
}
}
The ApplicationExceptionHandler class must implement IExceptionHandler interface:
internal class ApplicationExceptionHandler : IExceptionHandler
{
public bool HandleException(Exception e)
{
Trace.WriteLine(e.GetType()+" is caught");
return false; //return true to rethrow
}
}
{
public bool HandleException(Exception e)
{
Trace.WriteLine(e.GetType()+" is caught");
return false; //return true to rethrow
}
}
Hi Simon , it does not work with recursive calls. Any help? is the source code available?
ReplyDeleteWhat is error message, can you provide code snippet to show the problem?
DeleteI will open source it after port it to .netstandard.
For a recursive call, the aspect is invoked only once at first entrance and last exit, this is by design for performance.
DeleteHello Dear Brookside,
ReplyDeletei really loved your Fody add-on which is the best fit for my need.
To show my appriciation and gratitudes, i prepared a surprise for you: i ported this super CompileTimeWeaver to .NetStandard platform!
i just decompiled the .net assembly, then made necessary changes for .NetStandard compatibility, then recompiled. everything worked as expected.
i want to send you the updated version so that you can make it open source. Please be sure that i won't publish my version.
P.S. i made some customization in my version e.g. stripped out NuGet package generation stage and unit tests.
Brookside, i am just trying to give you the beauty back, which i already got from you ;)
So, What is your preferred way to get my version of CompileTimeWeaver?
I'm unable to get this to work with a .Net Standard 2.0 project. I installed the Nuget package (v3.1.7) but the namespace is not available so I'm not able to create anything.
ReplyDeleteCan you ask the same question under this new post for v3.1.7? https://brooksidebeauty.blogspot.com/2019/02/compiletimeweaverfody-v317.html, and create a simple example for me to test.
Delete