Aspect-oriented programming in .NET with AspectInjector

Yuriy Ivon
5 min readApr 17, 2019

--

AspectInjector — a framework, which allows applying aspects during compile-time and has a simple but flexible interface. In comparison with more frequently used run-time proxy-generating frameworks (e.g., Unity, Sprint.NET), compile-time AOP obviously gives much better performance, which is very important in some cases.

It has been three years since its most stable beta version was published, and since then we made numerous improvements eventually issuing the second major release. There were breaking changes in API between major releases, which is not comfortable for those who have been using AspectInjector already, but we are glad to say that we don’t expect so fundamental API changes anymore.

Let’s start with a simple example of what AspectInjector can easily do.

Imagine we need a simple trace for all methods — just capture method start and finish. In order to accomplish this, you will need to define an aspect implementing the desired behavior:

To apply the aspect to any class which needs it, just mark the target class with the attribute you’ve just created:

As a result, calls to TraceStart and TraceFinish will be injected to all methods of SampleService.

Let’s look into the attributes used to define the aspect in more detail.

  • Aspect annotates a class containing an aspect. Its mandatory parameter specifies how aspect’s instances are supposed to be created – a single instance for the whole application (Scope.Global) or a new instance for each object this aspect is injected into (Scope.PerInstance). This attribute has also an optional parameter Factory specifying a factory type used to instantiate aspects, an example of its usage can be found later in the article.
  • Injection annotates an attribute to be used to apply the specified aspect class to targets. The only mandatory parameter accepts an aspect type to be applied, and the remaining optional Priority parameter allows to control the order of aspect injection: the higher its value is, the earlier methods of this aspect will be executed.
  • Advice annotates a method which will be injected as a part of the containing aspect. It has one mandatory parameter Kind specifying how the annotated method will be applied to targets: Kind.Before – instructs to inject the advice method right before calling the target; Kind.After – instructs to inject the advice method right after the return from the target; Kind.Around – allows wrapping the original target by the specified advice method. To make it possible, the advice method may have two parameters corresponding to the target’s delegate and target’s call parameters. Non-mandatory parameter Targets of this attribute is also very important – it allows to control what kind of class members (getters, setters, regular methods, constructors, static members etc.) this aspect should be applied to. The full list of possible values can be found in Target enumeration and every value there is self-descriptive. The default value of Targets parameter is Target.Any, meaning that the advice will be injected for each class member.
  • Argument annotates advice parameters specifying what information about the target should be passed via this argument. Every value of the “source” parameter of this attribute require a specific advice argument type:Source.Instance (object) – an object instance this advice has been injected to; Source.Type (Type) – target type; Source.Method (MethodBase) – target’s metadata; Source.Target (Func<object[], object>) – a delegate to the target; Source.Name (string) – a name of the target; Source.Arguments (object[]) – original call arguments; Source.ReturnValue (object) – a value returned by the original call; Source.ReturnType (Type) – target’s return type;Source.Injections (Attribute[]) – a list of injection attributes that added this advice.

The logging example from above may raise questions on how to pass a logger instance if we want to use a logging framework instead of just writing to the console. There are two ways:

  • Have logger or logger factory accessible via a static property and use it directly in the aspect code. Obviously, it is not ideal, since it doesn’t allow to use dependency injection technique.
  • Have a factory class that handles all dependency concerns inside and has a static GetInstance(Type aspectType) method creating aspects of the specified type.

Let’s look at a possible implementation of the second approach in relation to the logging example from above. Apart from adding an aspect factory, we will enhance the logging aspect the way that it measures wrapped call time.

Here you can see that a separate class was created for the attribute since now aspect class needs some external dependencies to be passed in, but the attribute’s constructor shouldn’t have any parameters.

Factory class specified in aspect’s definition must have a static method GetInstance with the signature from the example above: it accepts a type and returns an object. All details of object creation can be hidden inside the method, it can use DI containers to achieve the goal. The only downside of this approach in comparison with “traditional” DI containers usage is the necessity to have this static “service locator”. Unfortunately, there is no other way to do it with compile-time AOP — the factory method must be available at compile time.

And the last, but not least — “around” kind of advice. It allows to completely wrap a call within a new method and introduce any additional logic before the original call and after it. In this example, a stopwatch is created before the underlying call and then used to calculate its duration. Without “around” kind of advice it would be quite challenging to reliably pass some state from “before call” context to “after call”. Please note that any “around” advice method must have at least two parameters — the target method delegate (Source.Target) and original call parameters (Source.Arguments).

Though “around” advice is a very powerful feature, there are some limitations to consider — it doesn’t support async/await out of the box. Here is an example of an alternative solution — https://github.com/pamidur/aspect-injector/issues/77. We are going to add async/await support to the framework in the future.

Let’s also take a look at one more important feature of Aspect Injector — interface implementations injection. The following example demonstrates how to make arbitrary classes implement INotifyPropertyChanged interface the way that every public property calls PropertyChaged event handlers every time its setter is called.

As you can see, to make an aspect that injects interface implementation it is enough to realize needed interface within the aspect and annotate it with Mixin attribute specifying the interface to be injected to target classes. Every class annotated with Notify attribute from above will get INotifyPropertyChanged and its implementation during compilation.

Overall this library can help in many situations when your application framework doesn’t provide any way to inject into action processing pipeline. And since it implies compile-time injection, it gives much better performance than alternative proxy-based AOP frameworks.

--

--