Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Yet Another Aspect-Oriented Programming Framework for .NET

4.74/5 (10 votes)
14 Aug 2016CPOL3 min read 24.5K  
Shows an introduction to a new AOP framework for .NET - AspectInjector

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:

C#
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:

C#
[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:

C#
public class SampleClass
{
    //Added by AspectInjector
    private TraceAspect __a$i_TraceAspect;

    public void Method1()
    {
        //Added by AspectInjector
        this.__a$i_TraceAspect.TraceStart(typeof(SampleClass), "Method1");

        Console.WriteLine("Inside Method1");

        //Added by AspectInjector
        this.__a$i_TraceAspect.TraceFinish(typeof(SampleClass), "Method1");
    }

    public int Method2()
    {
        //Added by AspectInjector
        this.__a$i_TraceAspect.TraceStart(typeof(SampleClass), "Method2");

        Console.WriteLine("Inside Method2");

        //Added by AspectInjector
        int num = 1;
        int result = num;
        this.__a$i_TraceAspect.TraceFinish(typeof(SampleClass), "Method2");
        return result;
    }

    public SampleClass()
    {
        //Added by AspectInjector
        this.__a$_initializeInstanceAspects();
    }

    //Added by AspectInjector
    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:

C#
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:

C#
[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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)