High-Performance AOP Framework AspectInjector Tutorial
AspectInjector—— A free and open-source framework that allows for the static injection of aspects into applications during compile-time, featuring a simple yet flexible interface. Compared to more commonly used runtime proxy generation frameworks (such as Unity
and Castle.Core.AsyncInterceptor
), compile-time AOP clearly offers better performance, which is crucial in certain scenarios.
It has been five years since the most stable beta version of AspectInjector was released. During this time, the development team has made multiple improvements and significant changes, and the API has ultimately been stabilized.
Now, I will start with a simple example to illustrate what AspectInjector can easily accomplish.
Consider a scenario where you need to perform simple tracking for all methods — just capturing the beginning and the end of each method. To achieve this, you need to define an aspect that implements the desired behavior:
public sealed
To apply the aspect to any class that requires it, simply mark the target class with the newly created attribute.
Therefore, the calls to TraceStart
and TraceFinish
will be injected into all methods of SampleService
.
Now, let’s delve into the attribute used to define the aspect in more detail. For the sake of understanding in the following text, in the example code above, the aspect class and the attribute class are the same, namely TraceAspectAttribute
.
The
Aspect
annotation is applied to the aspect class. Its mandatory parameter specifies how the aspect instance should be created — either a single instance for the entire application (Scope.Global
) or a new instance for each target object (Scope.PerInstance
). This attribute also has an optional parameter called Factory, which is used to specify the factory class for instantiating the aspect. An example of its usage can be found later in this text.Injection
annotates the specified aspect class on the attribute applied to the target (SampleService
). It has only one mandatory parameter, which is used to accept the type of the aspect class to be applied (typeof(TraceAspectAttribute)
). The optional priority parameter allows control over the order of aspect injection: the higher the value, the earlier the methods of this aspect will be executed.The
Advice
annotation is applied to methods of the aspect class, which will be injected into the target as part of the aspect. It has a mandatory parameter called Kind, which specifies how the annotated method will be applied to the target:Kind.Before
indicates that the method (annotated withAdvice
) should be injected before the call to the target.Kind.After
indicates that the method (annotated withAdvice
) should be injected immediately after returning from the target;Kind.Around
allows the original target to be wrapped by the specified method (annotated withAdvice
). To make this possible, the method (annotated withAdvice
) may have two parameters, corresponding to the delegate of the target and the call arguments of the target. The optional parameterTargets
is also very important—it allows control over which kind of class members the aspect should be applied to (getters, setters, regular methods, constructors, static members, etc.). A complete list of possible values can be found in theTarget
enum, with each value being self-descriptive. The default value for the Targets parameter isTarget.Any
, which means that theAdvice
will be injected for every class member.The
Argument
attribute is used for the parameters of methods annotated withAdvice
, specifying which information about the target should be passed through this parameter. Each value of the “source” parameter of this attribute requires a specificAdvice
parameter type:Source.Instance
(object) - the object instance into which this Advice has been injected;Source.Type
(Type) - the target type;Source.Method
(MethodBase) - the metadata of the target;Source.Target
(Func<object[], object>) - the delegate of the target;Source.Name
(string) - the name of the target;Source.Arguments
(object[]) - the original call arguments;Source.ReturnValue
(object) - the value returned by the original call;Source.ReturnType
(Type) - the return type of the target;Source.Injections
(Attribute[]) - the list of injection attributes that added thisAdvice
.
The logging example above might raise questions about how to pass the logger instance if you want to use a logging framework instead of just writing to the console. There are two methods:
- Access the logger or logger factory through a static property and use it directly in the aspect code. Obviously, it is not ideal because it does not allow the use of dependency injection techniques.
- There is a factory class that handles all internal dependency issues and has a static
GetInstance
(Type aspectType) method for creating aspects of the specified type.
Let’s look at a possible implementation of the second method related to the logging example above. In addition to adding an aspect factory, we will also enhance the logging aspect to encapsulate the measurement of call duration.
public sealed
public sealed
public
Here, you can see that a separate class is created for the attribute, because now the aspect class needs to accept some external dependencies, but the constructor of the attribute should not have any parameters.
In the definition of the aspect, the specified factory class must have a static method GetInstance
, as shown in the signature in the example above: it accepts a type and returns an object. All the details of object creation can be hidden inside the method, and a DI container can be used to achieve the target. The only drawback of this method compared to the use of “traditional” DI containers is that you must have this static “service locator”. Unfortunately, there is no other way to achieve this with compile-time AOP—the factory method must be available at compile time.
Finally, and certainly not least, is the Kind.Around
of Advice
. It allows the call to be completely wrapped in a new method, introducing any other logic before and after the original call. In this example, a stopwatch is created before the underlying call and then used to calculate its duration. Without the Kind.Around
of Advice
, reliably passing some state from the “before call” context to the “after call” would be quite challenging. Note that any method with Kind.Around
of Advice
must have at least two parameters—the target method delegate (Source.Target
) and the original call arguments (Source.Arguments
).
Let’s also take a look at another important feature of Aspect Injector——interface implementation injection. The following example demonstrates how to make any class implement the INotifyPropertyChanged
interface, as if the PropertyChanged
event handler were called every time the setter of each public property is invoked.
public
internal
As you can see, to create an aspect that injects an interface implementation, you simply need to implement the required interface within the aspect and annotate it with the Mixin
attribute, specifying the interface to be injected into the target class. Each class annotated with the Notify
attribute above will receive the INotifyPropertyChanged
interface and its implementation at compile time.
Overall, this library can be helpful in many cases when the application framework does not provide any methods for injecting operation handling pipelines. Moreover, since it implicitly implements compile-time injection, it offers better performance compared to AOP frameworks based on dynamic proxies.
This article is excerpted from: Aspect-oriented programming in .NET with AspectInjector