zaterdag 2 augustus 2008

Simplify your life... decorate with aspects !

In the company that currently hired me tracing for instance (which is one of the areas you could benefit a lot from using aspects), is done my hand.  Every method (at least most of them)  follows the next format

public bool Method(params string[] inputValues) { Trace("Enter", "MethodName", inputValues); // trace all input parameters ..... bool someValue; ...... Trace("Exit", "MethodName", someValue); // trace parameters leaving the method return someValue }

If you only have to write code once and then never touch it again, maybe this way of developing would be acceptable.  But I rarely (read never) have been on a project where this was true.  Code will be refactorred and not only once.  Keeping the tracing parameters in sync with the constant refactorings is something that is often forgotten.  Compare it with the method documentation you write, if you would generate your ndoc now does it truly reflect your code??  My thoughts exactly, so help is needed, aspect orientation to the rescue.  If you Google aspect orientation (that's right I even saved you the time to do that) you will find many useful resources and implementations, today I will be taking a closer look into PostSharp

PostSharp performs a post compilation step to inject IL code, which means it can 'adapt' your entire code base even private methods,fields...  You can use PostSharp in two ways, the easiest is to download the msi file and install it on your machine.  If you are anything like me you hate installing software on your machine, you might have a look at this blog post from Gael Fraiteur the lead developer for the project.  He explains what happens under the hood and how the post compile step is called in msbuild.   

If now you add the required assemblies (PostSharp.public and optionally PostSharp.Laos) to your project, the post compilation step will automatically gets called from msbuild.  It will search your assembly for all required tasks to process and then updates the IL code.

First on my list is to improve my above tracing problem.  This is easily solved by overriding the OnMethodBoundaryAspect (provided as one of the higher level aspects of the PostSharp.Laos library) , this provides the following methods:

  • OnEntry : called when entering the method
  • OnExit : always called when exiting a method
  • OnException: called when the method resulted in an exception
  • OnSuccess: called when the method executed successfully

and leads to the following code:

public override void OnEntry(MethodExecutionEventArgs eventArgs) { Trace.WriteLine( string.Format("Enter {0}.{1}.", typename, methodname), this.category); } public override void OnExit(MethodExecutionEventArgs eventArgs) { Trace.WriteLine( string.Format("Leaving {0}.{1} with {2}", typename, methodname, eventArgs.ReturnValue), this.category); } public override void OnException(MethodExecutionEventArgs eventArgs) { Trace.WriteLine( string.Format("Exception {0}.{1}.", eventArgs.Exception.Message, eventArgs.Exception.StackTrace), this.category); } public override void OnSuccess(MethodExecutionEventArgs eventArgs) { // Usually you don't need this }

Another way to achieve this is to override the OnMethodInvocation.  This provides you with different methods to work with but you have the same functionality at your disposal.  The major difference is that this method does not modify your original method (as would happen with the above sample), but intercepts the call en invokes your OnInvocation method).  Check reflector to see the differences here!!

public override void OnInvocation(MethodInvocationEventArgs eventArgs) { // Perform tracing here Trace.WriteLine( string.Format("Enter {0}.{1}.", typename, methodname), this.category); try { base.OnInvocation(eventArgs); // Invoke your method here! } catch(Exception ex) { Trace.WriteLine( string.Format("Exception {0}.{1}.", ex.Message, ex.StackTrace), this.category); } finally { Trace.WriteLine( string.Format("Leaving {0}.{1} with {2}", typename, methodname, eventArgs.ReturnValue), this.category); } }

So far so good.  Applying these attributes is as simple as using other attributes in .NET, just decorate your method/class with them.  If off course you wish to perform tracing on an entire assembly or part of it, You will be adding a lot of these attributes, so a better way has been provided:  The following configuration intercepts all method calls from the Tracing.Model, Tracing.Repository and Tracing.Service namespace.    You could tweak this further.

[assembly: Trace(AttributeTargetTypes = "Tracing.Model.*")] [assembly: Trace(AttributeTargetTypes = "Tracing.Repository.*")] [assembly: Trace(AttributeTargetTypes = "Tracing.Service.*")]

You could specify on which the aspects (or attributes) can be applied by specifying the AttributeUsage and MulticastAttributeUsage, see the official documentation about that here (UserGuide -> Using PostSharp LAOS -> Wildcards and multicasting).

Secondly my I wanted to simplify my validation code.  One solution is to decorate my fields with the specification they have to obey.  For instance

  • A String field should contains maximally 16 characters
  • A Number must be between 1-100 (see the following code sample)
  • A Date must be in the future
[Serializable] public class RangeSpecification : OnFieldAccessAspect { public Int32 _minimum = 0; public Int32 _maximum = 100; public RangeSpecification(Int32 minimum, Int32 maximum) { _minimum = minimum; _maximum = maximum; } public override bool CompileTimeValidate(System.Reflection.FieldInfo field) { Console.WriteLine("RangeSpecification"); // this comes in the output window when compiling if (field.FieldType.Name != typeof(Int32).Name) { // "Why would you want to use this specification on a non number field?" return false; } return true; } public override void OnSetValue(FieldAccessEventArgs eventArgs) { Int32 exposedValue = Int32.Parse(eventArgs.ExposedFieldValue.ToString()); if (exposedValue < _minimum || exposedValue > _maximum) { // keep the old value when an invalid value was passed in eventArgs.ExposedFieldValue = eventArgs.StoredFieldValue; } base.OnSetValue(eventArgs); } }

So far the first exploration of PostSharp, I  realize this was only the top of the possibilities but I hope everybody can see how much code can be saved and might not clutter your production code by applying these aspects correctly in your code.

See you next time.

4 opmerkingen:

Frederik Gheysels zei
Deze reactie is verwijderd door de auteur.
Frederik Gheysels zei

Coping with cross-cutting concers like logging via Aspect Oriented Programming is something that is very usefull indeed, and I'm very interested in it as well.
A while ago, I played with [url="http://fgheysels.blogspot.com/2006/11/aspect-oriented-programming-in-net.html"]AOP using Spring.NET[/url].
However, I found one big disadvantage (IMHO), and that is that Spring.NET needs to create a proxy of your object.
He needs to do that, since he's weaving the aspects at runtime.

Interesting to hear (or rather, read :) ) that PostSharp does this at compile-time.
I wonder however: does PostSharp allows you to exactly specify on which methods / properties / ... your pointcuts should be weaved into; via attributes for instance ?
For example: is it possible to decorate some methods with a Custom Attribute (for instance [Log]), and PostSharp only injects the logging functionality in those methods that have been decorated with that [Log] attribute ?
(Somehow like I've mentionned / showed in my blog-post ? )

Frederik Gheysels zei

Hmm, the link I've posted doesn't work, since I used [url] syntax...

So, this is the link then:
click

Patrick zei

@Frederik

The aspects you create are actually .NET attributes. You can apply them on your method, field, class, .... (depending on the AttributeUsage you) pr as shown in the blogpost in the AssemblyInfo.cs

Usually I want to apply my tracing on an entire assembly to not clutter my entire code with custom attributes. But as mentioned there are different possibilities. I've adapted my post with a link to the documentation.