Last week, I wrote an introduction to the concept of Aspect Oriented Programming. As promised, here is the first of two follow-up posts that showcases the code necessary to implement an AOP framework in C#. In this first part I'll go over the architecture of the framework, and then in the second part, which I'll post in a few days, we'll start on the actual implementation. I've chosen a fairly simple form of AOP to implement, which is compositional, proxy-based AOP. I'll talk more about the implications of this choice towards the end of the post.
Pointcuts and Advice
The cornerstone of our AOP framework will be how the advice is applied to a target. We'll limit the capabilities of the framework to targeting advice for methods and properties, which will cover most use cases. From last weeks article, we remember that where an advice is applied is defined by a joinpoint, and a set of joinpoints make up a pointcut. We're not going to model joinpoints directly in our framework, since a pointcut by definition implicitly defines one or more of them by virtue of itself.
A pointcut, then, needs the ability to decide whether it applies to any given member. We can split this question into two parts - does the pointcut apply to type X, and if it does, does it apply to the specific member Y on that type? If the answer is yes, the pointcut can then be asked to supply us with the advice to apply to the member in question.
Chaining Advice
Any given member may be advised by zero, one or more advice. Thus, we need a way of chaining advice together, so they can be executed in order. Assuming there are n advice for a given member, we expect the advice to be invoked recursively towards the target (member), as shown in the figure below:
When each advice is executed, it can perform its before logic and then choose to proceed towards the target, walking down the advice stack. When we reach the end we invoke the target member, and the begin walking back up again as each advice executes any after logic and returns. Should an advice choose not to proceed on the way down, but instead return immediately, no further advice down the stack will be executed, and the target will neither be invoked. This is an important feature that can be used for caching and validation advice, for example.
Weaving the Proxy
Having defined a set of pointcuts and how the advice is execute on a target, we need a way to actually hook this up to the advised member. Assuming that we have some interface IFoo and an implementation of it Foo, we do this by generating a transparent proxy which implements IFoo and wraps an instance of Foo. When a client invokes a member on the proxy, the proxy then - instead of directly invoking the real member implementation - executes the advice stack as described above. Generating this proxy is the responsibility of the weaver. We'll be using the Reflection.Emit API to build the proxy at runtime; more on that next week.
The Architecture
Below are the central parts of the architecture so far discussed. Foremost is the ProxyFactory, which takes an instance of a given type implementing some given interface, looks up any pointcuts from some configuration that should be applied to it, and forwards them to the Weaver. The Weaver then generates a proxy for the interface, weaving in the advice defined by the pointcuts as a set of Advisors for each advised member. When the client later invokes a member on the proxy it is in reality the Advisor for the member which executes. Instead of directly invoking the implementation of the member on the real instance, the Advisor sets up an AdviceTarget and starts walking the stack of advice, as described earlier.
A Flawed Implementation?
As mentioned in the beginning, the form of AOP implemented here based around the notion of generating a runtime proxy. This requires that any type we want to apply advice to must be defined by an interface, since the framework will use it to build the proxy and inject the advice using a variant of the decorator pattern. There are some drawbacks to such an architecture - most notably that only calls through the interface will be advisable. This means that any calls directly between members internally in a class will not execute any advice applied to them. Say, for instance, that you have an interface with two members - a GetAllItems method and a Count property. If we implement this interface so that the Count property calls GetAllItems internally to figure out the number of items, and then apply an advice to the GetAllItems method that caches the items, then only external calls to the proxy will hit the cache while the implementation of the Count property itself will always go directly to the GetAllItems method. This behavior is something that users of the framework need to be aware of, as it can have significant side effects if not taken into consideration when applying advice. Incidentally, this is also how the several AOP frameworks (like Spring) is implemented.
There are other ways to inject the aspects that would circumvent this issue - for example, we could instead of using an interface to create our proxy demand that any advisable member needs to be virtual and then use inheritance to create our proxy. Another much more powerful solution - but also much more complicated to implement - would be to use the (unmanaged) Profiling API to directly inject advice into a type right before the CLR JITs it. The mocking framework TypeMock uses this approach to generate its mocks. These are all runtime solutions - we could also implement a compile-time framework, but this would take away the flexibility of the framework as any change in advice configuration would require a recompile.
That's it for this part - next time we'll look at the actual code needed to implement the framework as proposed in this post, complete with downloadable source code and examples. Make sure to grab my RSS feed (if you haven't already), and don't miss it!