Using AOP and PostSharp to Enhance Your Code

I've been looking at ways to improve the quality of my team's code by removing "unrelated" code from within methods. By that I mean things like opening transactions, caching and exception handling.

For instance, code like that might be quite common:

public object GetSomething()
        {
            
try
            {
                
object o = HttpContext.Current.Cache["MySomething"];
                
if (o == nullreturn o;
                
using (TransactionScope scope = new TransactionScope())
                {
                    o 
= DataAccess.ActuallyGetSomething();
                    scope.Complete();
                }
                HttpContext.Current.Cache[
"MySomething"= o;
                
return o;
            }
            
catch (Exception x)
            {
                ExcptionPolicy.HandleException(x, 
"MyPolicy");
            }
        }

As you can see, it might look like this code does a lot of things, but actually it's only data access wrapped with all these really required wrappers: transactions, caching, exceptions. And if I wanted to add logging in the beginning and end of every method there will be hell to pay...

Well, In reality it's not really that bad. The wrapping code is spread over several layers, and we're also using an anonymous method mechanism to reduce copy-pasting of code, but still. I want to get this ugliness out of my code.

This is what I want:

 

[EnsureTransaction]
[HandleException]
[Cached("MySomething")]
public object GetSomething()
{
return DataAccess.ActuallyGetSomething();
}

In order to reach this attributed heaven I knew I would probably have to use a technique I heard a lot about but never actually tried to use - Aspect Oriented Programming, or AOP. Among other things, AOP allows you to inject advice (some code or behavior) at join-points - locations in your code, like at beginning or end of a method. And it allows you to do this in a cross-cutting manner, which means you don't have to write any more code inside your method to make it work.

So I went to look for a .NET AOP framework that we might be able to use. I admit I didn't invest that much time in searching and checking out the different options (and believe me, there's plenty). I went for the first framework I found that allows me to write my beloved attributes as quickly as possible and to easily check if the damn thing even works.

The framework I finally found is called PostSharp, which is a post compiler for .NET. What's a post compiler? You might ask. Well, basically, a .NET post compiler lets you compile your code into an assembly, and then it changes your IL code. Freaky, eh? PostSharp achieves this by using some MS-Build tasks that hunts, among other things, these special attributes and replaces them with code. What kind of code? Well, whatever you tell it to.

Finally, I was able to implement the attributes I wanted. There's one:

[Serializable]
        [global::System.AttributeUsage(AttributeTargets.All, Inherited 
= false, AllowMultiple = true)]
        
public sealed class CachedAttribute : OnMethodInvocationAspect
        {
            
private readonly string _cacheKey;

            
public CachedAttribute(string cacheKey)
            {
                
this._cacheKey = cacheKey;
            }

            
public string CacheKey
            {
                
get { return this._cacheKey; }
            }

            
public override void OnInvocation(MethodInvocationEventArgs eventArgs)
            {
                
object cached = HttpContext.Current.Cache[CacheKey];
                
if (cached == null)
                {
                    cached 
= eventArgs.Delegate.DynamicInvoke(eventArgs.GetArguments());
                    HttpContext.Current.Cache[CacheKey] 
= cached;
                }
                eventArgs.ReturnValue 
= cached;
            }
        }

What you can see here, is an attribute that inherit's from PostSharp's OnMethodInvocationAspect base class. Any call to a method marked with my cached attribute will be replaced to a call to a different method, which first checks for the specified CacheKey (look at the OnInvocation method - the methodInstance parameter is a delegate to the wrapped method).

And there's another one:

[Serializable]
        [global::System.AttributeUsage(AttributeTargets.Method, Inherited 
= true, AllowMultiple = false)]
        
private sealed class HandleExceptionAttribute : OnMethodBoundaryAspect
        {
            
public override void OnEntry(MethodExecutionEventArgs eventArgs)
            {
                
base.OnEntry(eventArgs);
            }

            
public override void OnExit(MethodExecutionEventArgs eventArgs)
            {
                
base.OnExit(eventArgs);
            }

            
public override void OnException(MethodExecutionEventArgs eventArgs)
            {
                
base.OnException(eventArgs);
                ExceptionPolicy.HandleException(eventArgs.Exception, 
"General");
            }
        }

This is even simpler: Using the OnMethodBoundaryAspect, I can inject code at the beginning and end of the method. Here I'm using this to handle exceptions (using Enterprise Library ExceptionPolicy object), but I can use this for transactions/logging/whatever. Excuse me for not implementing EnsureTransactionAttribute for now, but I will leave that for the reader :)

And voila:

 

[HandleException]
[Cached("MySomething")]
public object GetSomething()
{
return DataAccess.ActuallyGetSomething();
}

In order to make all this work, you have to:

1. Download and install PostSharp. You should download the latest build from the daily builds page.

2. The install will modify your standard build process (more on that in Part B), so you will have to define the constant POSTSHARP in your project properties in VisualStudio, in order to tell the Post-Compiler to kick in and do the dirty stuff. (Project properties -> Build -> General -> Conditional Compilation Symbols).

3. And of course, you have to reference PostSharp's DLLs (specifically - PostSharp.Laos, PostSharp.Public) which are automatically installed in your GAC.

And that's it! Just use these attributes on your methods, compile (yeah, the post-compiling is gonna make this a bit longer) and see how the magic works.

Hope you enjoyed this. In part B we'll talk more about how this whole thing works, I'll show you some more tricks you can use, talk about some pitfalls you might want to avoid and conclude with the advantages and disadvantages of using a post compiler (That is, of course, if I won't change my mind by the time I write the second part and decide that I want to talk about flowers instead. Or something).

 

In part B

At the last part we talked a little about post compiling, AOP and how we can use the PostSharp tool to make our code look a lot better. At this part I want to get a little more deep inside the mechanism behind this cool feature, and I'll do this by first showing another example.

The Logging Attribute

Let's say you want to create an attribute that enables you to log a method, including when you entered the method, the arguments it received and it's processing time. Sounds useful, no? Let's see how easy it is to do it with PostSharp.

First we'll create our attribute.

 

[global::System.AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = false)]
public sealed class LoggingAttribute : OnMethodBoundaryAspect
{
public override void OnEntry(MethodExecutionEventArgs eventArgs)
{ }
public override void OnExit(MethodExecutionEventArgs eventArgs)
{ }
}

For now it does nothing, but you can see we already have some hooks we can use to log the entrance and the exit of the method. Also notice that we marked the attribute as Serilizable. It won't compile otherwise (we'll see why later on). In order to create the headers for our log messages, we'll override a different method: CompileTimeInitialize.

 

[Serializable]
[global::System.AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = false)]
public sealed class LoggingAttribute : OnMethodBoundaryAspect
{
private string _entranceMessage;
private string _exitMessage;

public override void CompileTimeInitialize(System.Reflection.MethodBase method)
{
base.CompileTimeInitialize(method);
string methodDescription = method.DeclaringType.FullName + ": " + method.Name;
_entranceMessage = "Entering " + methodDescription;
_exitMessage = "Exiting " + methodDescription;
}

public override void OnEntry(MethodExecutionEventArgs eventArgs)
{
}

public override void OnExit(MethodExecutionEventArgs eventArgs)
{
}
}

As you can see, this method gets a MethodBase as a parameter, which supplies us with the name of the method and it's enclosing type. We use this to create both an entrance message and an exit message. As the name suggests, CompileTimeInitialize is called during compile time, so the string concatenations that go on here we'll be called only when we compile method. It hardly matters for this small example, but may hold a sigfinicant performance value for more intense calculations. Anyways, let's move on and see the completed attribute:

 

[Serializable]
[global::System.AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = false)]
public sealed class LoggingAttribute : OnMethodBoundaryAspect
{
private string _entranceMessage;
private string _exitMessage;

public override void CompileTimeInitialize(System.Reflection.MethodBase method)
{
base.CompileTimeInitialize(method);
string methodDescription = method.DeclaringType.FullName + ": " + method.Name;
_entranceMessage = "Entering " + methodDescription;
_exitMessage = "Exiting " + methodDescription;
}

public override void OnEntry(MethodExecutionEventArgs eventArgs)
{
base.OnEntry(eventArgs);
Console.WriteLine(_entranceMessage);
object[] arguments = eventArgs.GetArguments();
if (arguments != null && arguments.Length > 0)
{
Console.WriteLine("Method arguments: ");
for (int i = 0; i < arguments.Length; i++)
{
if (i != 0) Console.Write(",");
Console.Write(arguments[i]);
}
Console.WriteLine();
}
eventArgs.MethodExecutionTag = DateTime.Now;
}

public override void OnExit(MethodExecutionEventArgs eventArgs)
{
base.OnExit(eventArgs);
Console.WriteLine(_exitMessage);
Console.WriteLine("Return value: " + (eventArgs.ReturnValue ?? "null"));
DateTime startTime = (DateTime) eventArgs.MethodExecutionTag;
TimeSpan processTime = DateTime.Now.Subtract(startTime);
Console.WriteLine("The processing of the method took " + processTime.TotalMilliseconds +
" milliseconds ");
}
}

At entry we are writing to the console (although we could use any other way of logging, of course) our entrance message, and the method arguments, if it has any. Notice that we also save the time we entered the method in the MethodExecutionTag property of the eventArgs, which allows us to save state for the method execution. In OnExit we're using this property to determine how long it took the method to run. We're also printing to screen the return value of the method (making sure that if the return value is null, we'll say so). Nice and easy.

So let's place this on a method:

 

public class NamesBO
{
[Logging]
public string WhatIsMyName(string firstName, string lastName)
{
Thread.Sleep(1000);
return firstName + lastName;
}
}

And now let's test this...

Great, although as you can see, I forgot to place a space between my first name and my last name. Oh well.

Digging In

Now that we've got our example working, let's see what goes on underneath. Unfortunately, Ludz' Reflector seems to have an issue with methods modified by PostSharp, and is unable to disassemble them. We'll have to brush up on our IL then. Let's look on parts of the WhatIsMyName method, using the IL DASM tool.

1 .method public hidebysig instance string 
 
2         WhatIsMyName(string firstName,
 
3                      string lastName) cil managed
 
4 {
 
5 .
 
6 System.Reflection.MethodBase::GetMethodFromHandle(valuetype [mscorlib]
 
7 
 
8   IL_0038:  newobj     instance void [PostSharp.Laos]PostSharp.Laos.MethodExecutionEventArgs::
.ctor(
class [mscorlib]System.Reflection.MethodBase,object,object[])
11 
12   IL_0041:  ldsfld     class Common.LoggingAttribute '~PostSharp~Laos~Implementation'::'aspect~2'
13 
14   IL_004a:  callvirt   instance void [PostSharp.Laos]PostSharp.Laos.IOnMethodBoundaryAspect::
OnEntry(
class [PostSharp.Laos]PostSharp.Laos.MethodExecutionEventArgs)
15 
16

 

Here we can see the parts of the injected code:

  1. At line 6: A call to get the method data using reflection.
  2. At line 8: Creation of the MethodExectutionEventArgsObject.
  3. At line 12: Retrieval of the attribute instance (apparently the attribute object is instanstiated once, and saved in a static field).
  4. At line 14: Call to the OnEntry method.

Let's continue:

1 .try
 
2   {
 
3     .try
 
4     {
 
5       IL_0074:  call       void [mscorlib]System.Threading.Thread::Sleep(int32)
 
6       
 
7       IL_007c:  call       string [mscorlib]System.String::Concat(string,
 
8                                                                   string)
 
9      
10     }  // end .try
11     catch [mscorlib]System.Exception 
12     {
13       
14        IL_0092:  ldsfld     class Common.LoggingAttribute '~PostSharp~Laos~Implementation'::'aspect~2'
15       
16        IL_009b:  callvirt   instance void [PostSharp.Laos]PostSharp.Laos.IExceptionHandlerAspect
::OnException(
class [PostSharp.Laos]PostSharp.Laos.MethodExecutionEventArgs)
17      PostSharp.Laos.MethodExecutionEventArgs::get_ReturnValue()
18      
19     }  // end handler
20   }  // end .try
21   finally
22   {
23    
24     IL_00ec:  callvirt   instance void [PostSharp.Laos]PostSharp.Laos.IOnMethodBoundaryAspect::OnExit
(
class [PostSharp.Laos]PostSharp.Laos.MethodExecutionEventArgs)
25 
26 PostSharp.Laos.MethodExecutionEventArgs::get_ReturnValue()
27     IL_00fa:  castclass  [mscorlib]System.String
28     IL_00ff:  stloc      V_1
29     IL_0103:  endfinally
30   }  // end handler
31 
32 } // end of method NamesBO::WhatIsMyName
33

 

Wow, that's a big piece of code there. Well, apparently PostSharps wraps our code with some try-catch-finally blocks. It does this, obviously, in order to be able to handle cases in which the method throws an exception (we still want to log the method in this case, don't we?)

  1. At lines 5-7: Our string concatenation takes place (first and last name).
  2. In 11-20: Handling of an exception, and a call to the attribute's OnException method (we didn't use it in our example, but you can guess what it's for).
  3. in 21-30: We are exiting the method, we call the OnExit method, and make sure we return the ReturnValue that we got from the MethodExectutionEventArgs (which means that, yes, the attribute can change the method's return value if it wants too).

The last thing I want to show you from the IL is a piece of the assembly manifest. There it is:

.mresource private '~PostSharp~Laos~CustomAttributes~'
{
// Offset: 0x00000000 Length: 0x000001E7
}

Interesting, PostSharp appears to embed a resource in our dll. The Reflector is thankfully able to show us what's in that resource:

We can see that among other things, the string "Entering Common.NamesBO: WhatIsMyName" is embedded in the dll. That's one of the strings we saved in our CompileTimeInitialize method. That's the reason we had to mark our attribute as Serializable - so all our fields we'll be serialized in the dll's resource.

Multicasting

Lets say that our NamesBO contains tons of methods, and just now we started to use PostSharp and created our LoggingAttribute. We want to have logging for all the methods, but we really don't want to go over the gazillion methods and add the attribute to them. What shall we do, then? Well, just this:

[assembly: Logging(AttributeTargetTypes="Common.BO.*")]

This magic line will cast our attribute on all the methods in the Common.BO namespace, and we're all set.

But what if we wanted to add logging to methods that are in a dll that we don't have the source for, which we can't post compile. For example, what if we wanted to trace all our calls to the System.Collections.Generic namespace? Well, there is a solution for that too, although it will require us to rewrite our attribute, to inherit from OnInvocationAspect, as we did with the CachedAttribute in Part A. We'll call this attribute LoggingExternal. You'll notice that it's pretty similar to the Logging attribute, with the main change being that we override the OnInvocation method, which actually replaces the call to the original method.

[

 

[Serializable]
[global::System.AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = false)]
public sealed class LoggingExternalAttribute : OnMethodInvocationAspect
{
private string _entranceMessage;
private string _exitMessage;

public override void CompileTimeInitialize(System.Reflection.MethodBase method)
{
base.CompileTimeInitialize(method);
string methodDescription = method.DeclaringType.FullName + ": " + method.Name;
_entranceMessage = "Entering " + methodDescription;
_exitMessage = "Exiting " + methodDescription;
}

public void LogEntry(MethodInvocationEventArgs eventArgs)
{
Console.WriteLine(_entranceMessage);
object[] arguments = eventArgs.GetArguments();
if (arguments != null && arguments.Length > 0)
{
Console.WriteLine("Method arguments: ");
for (int i = 0; i < arguments.Length; i++)
{
if (i != 0) Console.Write(",");
Console.Write(arguments[i]);
}
Console.WriteLine();
}
}

public void LogExit(MethodInvocationEventArgs eventArgs, DateTime startTime)
{
Console.WriteLine(_exitMessage);
Console.WriteLine("Return value: " + (eventArgs.ReturnValue ?? "null"));
TimeSpan processTime = DateTime.Now.Subtract(startTime);
Console.WriteLine("The processing of the method took " +

processTime.TotalMilliseconds +
" milliseconds ");
}

public override void OnInvocation(MethodInvocationEventArgs eventArgs)
{
LogEntry(eventArgs);
DateTime startTime = DateTime.Now;
eventArgs.Delegate.DynamicInvoke(eventArgs.GetArguments());
LogExit(eventArgs, startTime);
}
}

Any call to a method that this attribute is placed upon, will be replaced with a call to a new method that is wrapped with our logging code. That means, that unlike attributes that inherit from OnMethodBoundaryAspect, if you want an attribute that inherits from OnInvocationAspect to work, you have to post-compile the assembly that calls the method, and not the assembly in which the method resides. A common pitfall (well, at least I fell in it) would be to post-compile only the assembly with our BO methods, and then not understand why your OnInvocationAspect-inheriting (such as caching) attributes don't work.

After we create the attribute, we'll apply it attribute as follows:

[assembly: LoggingExternal(AttributeTargetAssemblies = "mscorlib",
AttributeTargetTypes = "System.Collections.Generic.*")]

The only problem with this code is, well, that it doesn't work. It worked for the System.Threading namespace, but it doesn't work for this namespace, and it doesn't even compile for some namespaces I use.

As you can tell, the PostSharp framework is not stable enough at the moment - In one case compiling my code caused my Visual Studio to crash. For now PostSharp is still in its beta stage, so the bugs can be understood.

Conclusion

AOP with post compiling has many advantages: clearer code, allows you to avoid duplication, and easily integrated into existing code. Also, the compile-time weaving of the code ensures pretty good performance. Of course, there are some disadvantages as well: Unexpected behavior might arise in some situations (attributes on attributes? Recursion?) and the attributes should be written very carefully, especially if they're multicasted on classes or assemblies. Also, you have to post-compile your code which makes the compilation stage a bit longer.

Still, I feel the advantages are greater than the disadvantages. Although I believe it's not stable enough at the moment to be integrated into production code, the PostSharp framework is a great one for this purpose, and I suggest that you give it a shot.

I will conclude with a question for you: remember the CachedAttribute from Part A? Well, I can only pass it a constant Cache key, since that's the only kind of parameter I can pass to an attribute. Usually, that's not good enough for cache keys (they tend to depend on the method's arguments). Any idea how can I improve on this behavior? I thought of several solutions, but none of them was solid enough. Your advice will be greatly appreciated :)

posted @ 2007-09-12 18:06  冰戈  阅读(977)  评论(1)    收藏  举报