Announcing SheepAOP (Part 2)

This post is the excerpt from yesterday’s presentation about the new SheepAOP project that I’ve just started recently.
All code exercises in this post can be found under SheepAop.Samples project as part of the SheepAop repository (http://sheepaop.codeplex.com/)

Part 1 – AOP for Everyone
Part 2 – SheepAOP Basics

Some Arbitrary Application

We need to start somewhere, so why not from this simple meaningless code?

class Program
{
    static void Main()
    {
        IoCConfig.Bootstrap();

        var result = "Elephant".CalculateAdsFee(10);
        Console.WriteLine("Index: {0}", result);

        Console.ReadLine();
    }
}

public static class StringHelper
{
    public static int CalculateAdsFee(this string text, int rate)
    {
        Console.WriteLine("Calculating {0}... ", text);
        if(text.Length < 15)
            return text.Length*rate;

        Console.WriteLine("Maximum fee!!");
        return text.Length*rate + 15;
    }
}

image

No surprise there. 8 characters in “Elephant”, charged at $10 rate, calculates into $80.

Your First Aspect

Now let’s create a pointcut to target StringHelper.CalculateAdsFee(string, int) method above. In this case, I’ll say all methods beginning with word “Calculate”, declared within any type ending with word “Helper”:

public static class DemoAspect
{
    [RegisterPointcut]
    public static void CalculateFeePointcut(PointcutRegistry reg)
    {
        reg.AddCurrentAssembly().Advise(m => m.Method
            .Name("Calculate*")
            .DeclaringType(t => t.Name("*Helper"))
            , DemoAdvice);
    }
}

We have our pointcut, and as you see, we advise it with “DemoAdvice”, which doesn’t exist yet. So let’s create one now (visual-studio or resharper will help you with this).

private static void DemoAdvice(MethodJointPoint jointpoint)
{
    Console.WriteLine("Demo advice for {0} from {1}", jointpoint.Args[0], jointpoint.Method);
    jointpoint.ReturnValue = 10000;
    Console.WriteLine("Leaving demo advice");
}

This method will never get executed. It will only be interpreted at the compile time to decorate your original StringHelper methods.

What we’ve just done there in this case, is that we have swapped completely the body of our original StringHelper method with these 3 lines of code we have just introduced in our advice. This is a crude way of creating an advice, we’ll see later a gentler way of doing this. But for now, let’s see the result:

image

Let’s see with the debugger, and keep hitting F11.

image
image
image

The execution jumps straight out from the CalculateAdsFee method, and returns 10000 immediately. This is because the whole method body of CalculateAdsFee() is no longer there, as we instructed SheepAOP to replace it with the new content, which simply returns 10000.

That’s rarely what we want. So let’s now change our DemoAdvice to the following:

private static void DemoAdvice(MethodJointPoint jointpoint)
{
    Console.WriteLine("Demo advice for {0} from {1}", jointpoint.Args[0], jointpoint.Method);
    jointpoint.Args[0] = "Sheep!!";

    JointPoint.Proceed(); // LOOK HERE!

    jointpoint.ReturnValue = (int) jointpoint.ReturnValue + 100;
    Console.WriteLine("Leaving demo advice");
}

JointPoint.Proceed() on line#6 only contains “throw NotSupportedException” if you execute it. But that’s fine because advice methods never actually get executed. Only intepreted as a template during compile time.
JointPoint.Proceed() is a keyword that will be used by SheepAop compiler as a marker that that’s where the original method body should go. So if we run the application again, we’ll get:

image
image

image

image

image

image

Between step#2 and step#3, you can see that the parameter text changes its value magically from “Elephant” to “Sheep!!”. On step #4, the method was returning 70*10=70, but the actual return value immediately changed to 170 on step#5. These behaviour changes are caused by the advice what we specified earlier: we change args[0]=”Sheep!!”, and we also increment the ReturnValue by 100 (hence resulting in 170).

You have seen the basic of what you can do with Advice. You can do pretty much what you can with a normal code. You can check if a value is within a certain range, and alter the flow of the execution.

Altering Specific Lines Of Code

This example will seem a bit crazy, that’s because I am using an incredibly unrealistic example, just to demonstrate the kind of thing you can do with SheepAOP. Here’s the initial code:

class CrazyProgram
{
    static void Main()
    {
        IoCConfig.Bootstrap();

        string str = null;
        Console.WriteLine("Index of z: {0}", str.IndexOf('z'));

        Console.ReadLine();
    }
}

That line#8 is surely going to crash spectacularly, and rightly so.

imageimage

Even if we put an aspect inside of the string.IndexOf(char) method, that won’t help since the method won’t even get called in the first place.

So instead, let’s now write a SheepAOP aspect to target that specific line of code.

public class CrazyAspect: IAspect
{
    [RegisterPointcut]
    public static void CrazyPointcut(PointcutRegistry reg)
    {
        reg.AddCurrentAssembly().Advise(r=> r.Instruction
            .MethodCall(
                m=> m.Name("IndexOf").DeclaringType(t=> t.Is()))
            .ExecutedIn(
                m=> m.DeclaringType(t=> t.Is())
            ), CrazyAdvise);
    }

    private static void CrazyAdvise(MethodCallJointPoint jointpoint)
    {
        jointpoint.TargetInstance = "crazy";
        JointPoint.Proceed();
    }
}

So there, we create a pointcut to target a specific instruction line in the code. There are several types of instruction we can target, for example, setting a field, getting a field, “new” instantiation, equality comparison, etc, but in this case, we want to target the “method call to string.IndexOf(char)” from any line of code “executed within any method in the CrazyProgram class”.

PS: The ExecutedIn() criteria can be handy to define different advices for a particular instruction executed from different contexts. For example, we can define that when we call “new Customer()” from within the VIP area, then we will return a new instance of VIPCustomer instead.

Now if we execute the application again, we’ll get:

imageimageimage

This time, the line passed through successfully, even though the str value is still null. What happens here is that we have just redirected the call to string.IndexOf(‘z’) method to an instance of string value “crazy”, rather than its original null instance, and hence returning 3 (the index of ‘z’ in ‘crazy’).

That was a silly example, but it shows you how you can use SheepAop to reach virtually any part of your code. Not only methods and properties.

Event Driven Aspects

I do NOT recommend you writing advices using the approaches I have been using in all previous examples. There are 2 disadvantages of it:

  • Your advice will get welded straight to the target assembly, meaning that you won’t be able to change your advices dynamically at runtime
  • Since your advice methods are copied across during compilation, and never actually gets called, you won’t be able to follow it with debugger. There’s too much invisible surprises involved, which is why I strongly discourage this approach. For instance, if you look back to your first Aspect that we wrote, our debugger shows some invisible magic at the beginning of your method that changes your “Elephant” into  a “Sheep”.
    image
    image

There is an extension method on the PointcutRegistry to alleviate this problem. It’s called: Listener. Now let’s change our DemoAspect from our first example to the following:

public class DemoAspect: IAspect
{
    [RegisterPointcut]
    public static void CalculateFeePointcut(PointcutRegistry reg)
    {
        reg.AddCurrentAssembly().Listen(m => m.Method
            .Name("Calculate*")
            .DeclaringType(t => t.Name("*Helper")));
    }

    public void Init()
    {
        var listener = Listener.Method(CalculateFeePointcut);
        listener.OnEntry += (j, c) =>
        {
            Console.WriteLine("Demo advice for {0} from {1}", j.Args[0], j.Method);
            j.Args[0] = "Sheep!!";
        };
        listener.OnExit += (j, c) =>
        {
            j.ReturnValue = (int)j.ReturnValue + 100;
            Console.WriteLine("Leaving demo advice");
        };
    }
}

You’re not actually losing any functionality. You can do almost anything you could do with the old way. There are 3 events you can subscribe to: OnEntry, OnExit, and OnException.
Now if you run the debugger again, you will get the following:

image
image

image

On step#2, the debugger steps through your event handler, where you can inspect all the runtime details about the jointpoints. Now you can see everything that is going on. There’s no more code that’s not visible from the debugger.

And since your advice is now event-driven, you can subscribe and modify your advice at runtime however you like.

You can, for example, use plugin architecture to attach to these events to affect the intimate behaviour of your application dynamically. This is normally done using MEF or IoC using some sort of service-location pattern that you put in place in your application as a set of predefined extension points. AOP offers a less intrusive way of doing it, where you are not forced to reshape your architecture in any specific way to allow an injection of dynamic behaviours to your application via plugins and events. CQRS is another exciting possibility (where your reporting-context would unobstrusively subscribe to specific “pointcuts” within your domain-models).

Sometimes you still need to use the plain Advice, usually if your advice is surrounding your pointcut, for example: stopwatch to time around your pointcut, transaction-scope, or disposable objects. But when you do need to use Advice, try to extract out all logic into a separate public method. Remember that only the content of your Advice method gets copied across. So if you put your logic in a separate method, you will be able to use debugger on it. So only use the Advice method to declare control-flow, and leave any logic out. Try to keep your advice as lean as possible, and you’ll get as little surprise as you can afford.

Shopping Cart

Finally, to refresh our memory, here’s the aspect example from our previous post about the shopping-cart.

public class PurchasingNotificationAspect: IAspect
{
    [RegisterPointcut]
    public static void SetProductPointcut(PointcutRegistry reg)
    {
        reg.AddCurrentAssembly()
            .Listen(r => r.Setter(p => p.StockQty));
    }

    private readonly INotificationService _notifier;

    public PurchasingNotificationAspect(INotificationService notifier)
    {
        _notifier = notifier;
    }

    public void Init()
    {
        Listener.PropertySet(SetProductPointcut).OnExit += (j, c) =>
        {
            if ((int)j.Value < 5)
                _notifier.Send(Notice.Restock, (Product) j.Instance);
        };
    }
}

Setup

To You need to attach SheepAop compiler to your project (containing your aspect definitions) to weave your assemblies (any assembly, doesn’t have to be the project assembly itself). For example, our SheepAop.Samples project has the following lines defined in its csproj file. You will need to do the same for any project using SheepAOP aspects.

<PropertyGroup>
   <SheepAopLocation>$(MSBuildProjectDirectory)\Libs</SheepAopLocation>
</PropertyGroup>
<UsingTask TaskName="PostCompileWeaveTask" AssemblyFile="$(SheepAopLocation)\SheepAop.Tasks.dll" />
<Target Name="AfterBuild">
   <PostCompileWeaveTask TargetFile="$(MSBuildProjectDirectory)\$(OutputPath)$(MSBuildProjectName).exe" Namespace="SheepAop.Samples.*" SheepAopLocation="$(SheepAopLocation)" />
</Target>

Where to Next?

In very near future, these are the things that will keep me busy:

  1. Mixin support
  2. Support for Attribute style aspects
  3. Add more aspect supports for many other IL constructs (e.g. c# “new” keyword, constructors, ==, etc).
  4. Groking AspectJ

What I’m not gonna do (but would love if anyone could help):

  1. External DSL (ala AspectJ)
  2. IDE integration (background analysis and indexing to speed up compilation time)

Feedback?

This project started as my learning attempt. I’m happy with all the learning gained in the past few days around IL, Mono.Cecil, and AOP in general. I’m still learning and experimenting with this project, and I am looking forward to hear any feedback and advice. I’ve put all the source-code out there, so feel free to explore, scrutinize, and I’d be pleased to hear any area that can be improved, both in the coding department (maintainability) as well as performance.

And of course, any contribution will be more than welcomed.

Download

You can download the full source-code of SheepAOP and all examples in this post (and the previous post) in the SheepAop main repository in Codeplex (http://sheepaop.codeplex.com/).

Advertisements

2 thoughts on “Announcing SheepAOP (Part 2)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s