High-Performance AOP Framework AspectInjector Tutorial

  • Updated on 28th May 2022

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:

[Aspect(Scope.Global)]
[Injection(typeof(TraceAspectAttribute))]
public sealed class TraceAspectAttribute : Attribute
{
    [Advice(Kind.Before, Targets = Target.Method)]
    public void TraceStart(
        [Argument(Source.Type)] Type type,
        [Argument(Source.Name)] string name)
    {
        Console.WriteLine($"[{DateTime.UtcNow}] Method {type.Name}.{name} started");
    }

    [Advice(Kind.After, Targets = Target.Method)]
    public void TraceFinish(
        [Argument(Source.Type)] Type type,
        [Argument(Source.Name)] string name)
    {
        Console.WriteLine($"[{DateTime.UtcNow}] Method {type.Name}.{name} finished");
    }
}

To apply the aspect to any class that requires it, simply mark the target class with the newly created attribute.

[TraceAspect]
class SampleService
{
    public void Method1()
    {
        //...
    }

    public void Method2()
    {
        //...
    }
}

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 with Advice) should be injected before the call to the target. Kind.After indicates that the method (annotated with Advice) should be injected immediately after returning from the target; Kind.Around allows the original target to be wrapped by the specified method (annotated with Advice). To make this possible, the method (annotated with Advice) may have two parameters, corresponding to the delegate of the target and the call arguments of the target. The optional parameter Targets 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 the Target enum, with each value being self-descriptive. The default value for the Targets parameter is Target.Any, which means that the Advice will be injected for every class member.

  • The Argument attribute is used for the parameters of methods annotated with Advice, specifying which information about the target should be passed through this parameter. Each value of the “source” parameter of this attribute requires a specific Advice 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 this Advice.

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.

[Injection(typeof(TraceAspect))]
public sealed class TraceAttribute : Attribute
{
}

[Aspect(Scope.Global, Factory = typeof(AspectFactory))]
public sealed class TraceAspect
{
    private readonly ILogger _logger;
    public TraceAspect(ILogger logger)
    {
        _logger = logger;
    }

    [Advice(Kind.Around, Targets = Target.Method)]
    public object Trace(
       [Argument(Source.Type)] Type type,
       [Argument(Source.Name)] string name,
       [Argument(Source.Target)] Func<object[], object> methodDelegate,
       [Argument(Source.Arguments)] object[] args)
    {
        _logger.LogInformation($"[{DateTime.UtcNow}] Method {type.Name}.{name} started");
        var sw = Stopwatch.StartNew();
        var result = methodDelegate(args);
        sw.Stop();
        _logger.LogInformation($"[{DateTime.UtcNow}] Method {type.Name}.{name} finished in {sw.ElapsedMilliseconds} ms");
        return result;
    }
}

public class AspectFactory
{
    public static object GetInstance(Type aspectType)
    {
        // Here, you can implement any instantiation method, and the following methods are just simple examples
        if (aspectType == typeof(TraceAspect))
        {
            var logger = new Logger();
            return new TraceAspect(logger);
        }
        throw new ArgumentException($"Unknown aspect type '{aspectType.FullName}'");
    }
}

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.

[AttributeUsage(AttributeTargets.Property)]
[Injection(typeof(NotifyAspect))]
public class Notify : Attribute
{
}

[Mixin(typeof(INotifyPropertyChanged))]
[Aspect(Scope.PerInstance)]
internal class NotifyAspect : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged = (s, e) => { };

    [Advice(Kind.After, Targets = Target.Public | Target.Setter)]
    public void AfterSetter(
        [Argument(Source.Instance)] object source,
        [Argument(Source.Name)] string propertyName)
    {
        PropertyChanged(source, new PropertyChangedEventArgs(propertyName));
    }
}

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