Introduction
There are cases when aspect-oriented programming is very helpful — for example, logging, auditing, security, etc. Though .NET platform doesn't have relevant instruments out-of-the-box, there are some third-party solutions providing such capability: PostSharp, Unity, Spring .NET, Castle Windsor, Aspect .NET... And that is not the full list. However, when choosing an AOP framework for a particular task, it is worth thinking about the mechanisms implementing the injection of crosscutting functionality, about their pros and cons. Generally speaking, two approaches can be identified:
- Compile-time injection (PostSharp)
- Proxy-class generation at run-time (Unity, Spring .NET, Castle Windsor)
Compile-time injection is the most favorable option, because it requires no additional computational overhead during program execution, which is especially important for mobile devices. Though usual proxy-class generation has simpler implementation, apart from computational overhead, it also has some limitations — methods and properties must belong to an interface or be virtual in order to be intercepted via a proxy-class.
PostSharp offers great capabilities for aspect-oriented programming, but it is a commercial product, so may be not acceptable for some projects. As an alternative, we have developed and are continuing to improve AspectInjector — a framework, which allows applying aspects during compile-time and has simple but flexible interface.
Using the Code
Let's take a quick look at what AspectInjector
can easily do. You can add the library to your project using the following command in the Package Manager console:
Install-Package AspectInjector -Pre
Currently only pre-release versions have full feature set, so "-Pre
" switch is needed to get the most complete capabilities of the Aspect Injector.
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:
public class TraceAspect
{
[Advice(InjectionPoints.Before, InjectionTargets.Method)]
public void TraceStart([AdviceArgument(AdviceArgumentSource.Type)] Type type,
[AdviceArgument(AdviceArgumentSource.Name)] string name)
{
Console.WriteLine("[{0}] Method {1}.{2} started", DateTime.UtcNow, type.Name, name);
}
[Advice(InjectionPoints.After, InjectionTargets.Method)]
public void TraceFinish([AdviceArgument(AdviceArgumentSource.Type)] Type type,
[AdviceArgument(AdviceArgumentSource.Name)] string name)
{
Console.WriteLine("[{0}] Method {1}.{2} finished", DateTime.UtcNow, type.Name, name);
}
}
This definition means that methods TraceStart
and TraceFinish
will be called at the beginning and at the end of each method correspondingly, and each call will have the information about target method’s name and its declaring type.
In order to apply the aspect to any class which needs it, just mark the target class with Aspect
attribute:
[Aspect(typeof(TraceAspect))]
public class SampleClass
{
public void Method1()
{
}
public int Method2()
{
}
}
As the result, calls to TraceStart
and TraceFinish
will be injected to all methods of SampleClass
. Here is the decompiled resulting code:
public class SampleClass
{
private TraceAspect __a$i_TraceAspect;
public void Method1()
{
this.__a$i_TraceAspect.TraceStart(typeof(SampleClass), "Method1");
Console.WriteLine("Inside Method1");
this.__a$i_TraceAspect.TraceFinish(typeof(SampleClass), "Method1");
}
public int Method2()
{
this.__a$i_TraceAspect.TraceStart(typeof(SampleClass), "Method2");
Console.WriteLine("Inside Method2");
int num = 1;
int result = num;
this.__a$i_TraceAspect.TraceFinish(typeof(SampleClass), "Method2");
return result;
}
public SampleClass()
{
this.__a$_initializeInstanceAspects();
}
private void __a$_initializeInstanceAspects()
{
if (this.__a$i_TraceAspect == null)
{
this.__a$i_TraceAspect = new TraceAspect();
}
}
}
It was one of the simplest examples, which doesn’t show all capabilities of AspectInjector
. There are more options on injection targets, target members filtering (controlled by additional parameters on Aspect
attribute) and advice parameter sources. All this information can be found in the documentation.
There are some features, which deserve special attention — for example, "Around" injection point. It is currently available only in pre-release version of Aspect
Injector and allows to fully wrap any calls. One of the simplest examples here is tracing call durations:
public class DurationAspect
{
[Advice(InjectionPoints.Around, InjectionTargets.Method)]
public object TraceDuration([AdviceArgument(AdviceArgumentSource.Type)] Type type,
[AdviceArgument(AdviceArgumentSource.Name)] string name,
[AdviceArgument(AdviceArgumentSource.Target)] Func<object[], object> target,
[AdviceArgument(AdviceArgumentSource.Arguments)] object[] arguments)
{
var sw = new Stopwatch();
sw.Start();
var result = target(arguments);
sw.Stop();
Console.WriteLine("[{0}] Method {1}.{2} took {3} ms",
DateTime.UtcNow, type.Name, name, sw.ElapsedMilliseconds);
return result;
}
}
The main idea is that advices with InjectionPoints.Around
must accept function delegate and corresponding parameters — all necessary information to make the original call. Advice
method can do any extra work before and after calling the original method like shown in the example. However, "around
" injection is heavier than "before
" and "after
" in terms of code changes. Whilst the last two modify the body of the original method, "around
" creates additional wrapping functions, so it is recommended to use it only in case there is some data to be passed between start and end of a method.
One more important feature is interface injection. You may need to inject not only extra code to some methods, properties or events, but also extra interfaces to a class. A classic example here is INotifyPropertyChanged
for .NET UI frameworks:
[AdviceInterfaceProxy(typeof(INotifyPropertyChanged))]
public class NotifyPropertyChangedAspect : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = (s, e) => { };
[Advice(InjectionPoints.After, InjectionTargets.Setter)]
public void RaisePropertyChanged(
[AdviceArgument(AdviceArgumentSource.Instance)] object targetInstance,
[AdviceArgument(AdviceArgumentSource.Name)] string propertyName)
{
PropertyChanged(targetInstance, new PropertyChangedEventArgs(propertyName));
}
}
After that, you will have to decorate all view model classes with [Aspect(typeof(NotifyPropertyChangedAspect))]
attribute, and all their properties will be automatically made notifiable.
You can find more details about available attributes and parameters on the project page. We would appreciate any feedback and suggestions for AspectInjector
!
History
- 16th August, 2016: Initial version