Time for part two of Introduction to PostSharp, part one can be found here Introduction to PostSharp - Part 1.
In part two we will focus on PostSharp.Core. With PostSharp.Laos we get nice base classes that we just add our code to and PostSharp dose the rest for us, what I did not tell you in the first part is that PostSharp.Laos is a easy to use addin to PostSharp that is written with PostSharp.Core and as you my expect PostSharp.Core is not as easy to get a handle on.
The code for this example can be downloaded here, CacheExample.zip (303.53 kb), you need Visual Studio 2008 and PostSharp 1.5 RTM to compile and run the code.
In this part we will create a cache plugin for PostSharp using PostSharp.Core.
What it will do is that at method entry generate a cache key based on the parameters, check if there is a values saved for this cache key and if there is return that value otherwise continue with the method. At the end of the method, at successfull exit, we will save the return value, using the cache key generated at method entry, in the cache for future use.
Lets start with the basic example program we have,
public class Program
{
static void Main(string[] args)
{
string data;
do
{
Console.Write("Number: ");
data = Console.ReadLine();
Console.WriteLine("And the magic number is: {0}", DoBigCalculation(data));
} while (data != "quit");
}
private static int DoBigCalculation(string data)
{
int number;
if (int.TryParse(data, out number))
{
//This is just to simulate a big calculation/data operation you may have in your application
//that can be much fast with cache
System.Threading.Thread.Sleep(2000);
return number * 2;
}
return 0;
}
}
The DoBigCalculation method is the slow method that we want to cache because it's just to slow in it's current form.
The cache system we will use is very simple,
public static class Cache
{
private static Dictionary
Right now we have everything we need to cache the DoBigCalculation method and we could rewrite the method to look like this,
private static int DoBigCalculation(string data)
{
object cacheValue = Caching.Cache.Get(Caching.Cache.GenerateKey(data));
if (cacheValue != null)
{
return (int)cacheValue;
}
int number;
if (int.TryParse(data, out number))
{
//This is just to simulate a big calculation/data operation you may have in your application
//that can be much fast with cache
System.Threading.Thread.Sleep(2000);
number = number * 2;
}
else
{
number = 0;
}
Caching.Cache.Set(Caching.Cache.GenerateKey(data), number);
return number;
}
and that works perfecly fin, but now we have added caching code to our calculation code and the calculation code is now responsible to have both working caching code and working calculation code. What we want to do insted is be able to write the caching code sepereate from the calculation code and then just be able to mark the method for caching and the caching code will be added to the method automaticly during compile time, to do this we write a caching aspect with PostSharp.Core and when we are finished the resulting code, after compile, will look very much like the example above but when we look at the code in our project it will still look like the code in the first example and only contain calculation code.
Lets begin by creating the caching attribute we want to use,
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property,
AllowMultiple = false, Inherited = false)]
[MulticastAttributeUsage(MulticastTargets.Method | MulticastTargets.Property, AllowMultiple = false)]
[RequirePostSharp("Aspects.Weaver", "Aspects.Weaver")]
public class CacheAttribute : MulticastAttribute
{
}
Now when we have a attribute its time to create the two classes that will form our aspect, the Task and Advice, as always for more information about exacly why and what check out PostSharp.org. Lets begin with the Task,
public class CacheTask : Task, IAdviceProvider
{
protected override void Initialize()
{
}
#region IAdviceProvider Members
public void ProvideAdvices(PostSharp.CodeWeaver.Weaver codeWeaver)
{
}
#endregion
}
and then continue with the advice,
public class CacheAdvice : IAdvice
{
private readonly CacheTask _parent;
private readonly CacheAttribute _attribute;
private LocalVariableSymbol _cacheKeyVariable;
public CacheAdvice(CacheTask parent, CacheAttribute attribute)
{
_parent = parent;
_attribute = attribute;
}
#region IAdvice Members
public int Priority
{
get { return 5; }
}
public bool RequiresWeave(WeavingContext context)
{
return true;
}
public void Weave(WeavingContext context, PostSharp.CodeModel.InstructionBlock block)
{
}
private void WaveEntry(WeavingContext context, InstructionBlock block)
{
}
private void WaveExit(WeavingContext context, InstructionBlock block)
{
}
#endregion
}
One more thing needs to be added to the project, a file that binds the attribute and Task togerther, the Aspect.Weaver.psplugin file,
This plugin file has to be in the bin directory of the example program for it to work.
Lets start with the code for the task.
public void ProvideAdvices(PostSharp.CodeWeaver.Weaver codeWeaver)
{
// Gets the dictionary of custom attributes.
AnnotationRepositoryTask annotationRepository = AnnotationRepositoryTask.GetTask(this.Project);
// Requests an enumerator of all instances of our CacheAttribute.
IEnumerator customAttributeEnumerator = annotationRepository.GetAnnotationsOfType(typeof(CacheAttribute), false);
// For each instance of our CacheAttribute.
while (customAttributeEnumerator.MoveNext())
{
// Gets the method to which it applies.
MethodDefDeclaration methodDef = customAttributeEnumerator.Current.TargetElement as MethodDefDeclaration;
if (methodDef != null)
{
// Build an advice based on this custom attribute.
CacheAdvice advice = new CacheAdvice(this, null);
// Add join points at the start of the method and at the end of a successfull method
codeWeaver.AddMethodLevelAdvice(advice, new Singleton(methodDef), JoinPointKinds.BeforeMethodBody | JoinPointKinds.AfterMethodBodySuccess, null);
}
}
}
This code will call the advice for every instance of our cache attribute in the code and tell the advice that we want to add code to the method at the top of the method (BeforeMethodBody) to be able to check if the value requested can be found in our cache or not and then after a successfull running of the method to be able to add the return value to the cache and return the value.
Now we need to add helper methods to the Task, this are instances of the cache methods that needs to be called in the advice,
internal IMethod CacheSetMethod;
internal IMethod CacheGetMethod;
internal IMethod CacheGenerateKeyMethod;
internal ITypeSignature ObjectType;
internal ITypeSignature ObjectArrayType;
This are variables that will containe methods and types that are needed by the advice.
We override the initialize method to initialize the variables,
protected override void Initialize()
{
ModuleDeclaration module = this.Project.Module;
ObjectType = module.FindType(typeof(object), BindingOptions.Default);
ObjectArrayType = module.FindType(typeof(object[]), BindingOptions.Default);
this.CacheSetMethod = module.FindMethod(typeof(Caching.Cache).GetMethod("Set", new Type[]
{
typeof(object),
typeof(object)
}), BindingOptions.Default);
this.CacheGetMethod = module.FindMethod(typeof(Caching.Cache).GetMethod("Get", new Type[]
{
typeof(object)
}), BindingOptions.Default);
this.CacheGenerateKeyMethod = module.FindMethod(typeof(Caching.Cache).GetMethod("GenerateKey", new Type[]
{
typeof(object[])
}), BindingOptions.Default);
}
We use the FindType and FindMethod methods to find the types and methods we need to work with later.
Now when the Task class is ready its time to move on to the advice.
First we add code to our Weave method,
switch (context.JoinPoint.JoinPointKind)
{
case JoinPointKinds.BeforeMethodBody:
this.WaveEntry(context, block);
break;
case JoinPointKinds.AfterMethodBodySuccess:
this.WaveExit(context, block);
break;
default:
throw new ArgumentException(string.Format("Unexpected join point kind: {0}", context.JoinPoint.JoinPointKind));
}
This is the method that will be called from our Task whenever a attribute is found and when its time to add code to one of our defined Joinpoints.
Now we get into the hard part, the code generation, lets start with WaveEntry, here we want to generate a cache key, check if the cache contains a object and return that object or if it's null continue with the method.
I have added alot of code comments here so read them to get a unerstanding of what happens here,
private void WaveEntry(WeavingContext context, InstructionBlock block)
{
// Create a new instruction sequence and add it to the block
// dedicated to our advice. Attach the InstructionWriter.
InstructionSequence entrySequence = context.Method.MethodBody.CreateInstructionSequence();
// Create another instruction sequence that we will jump to if nothing was found in the cache
InstructionSequence endSequence = context.Method.MethodBody.CreateInstructionSequence();
// Add the instruction sequence at the top of the method
block.AddInstructionSequence(entrySequence, NodePosition.Before, null);
// Add the instruction sequence after the previus instruction sequence
block.AddInstructionSequence(endSequence, NodePosition.After, entrySequence);
// Attach the instruction sequence to the writer so that we can being writing code
context.InstructionWriter.AttachInstructionSequence(entrySequence);
// Hide the sequence from debuggers.
context.InstructionWriter.EmitSymbolSequencePoint(SymbolSequencePoint.Hidden);
// Create a local variable of type object, this will hold the cache key we create for this method call and
// will be used both there in Entry and in the Exit method
_cacheKeyVariable = block.DefineLocalVariable(_parent.ObjectType, "~cache~key~{0}");
// Create a local variable that will be used as the argument to the generate cache key method
LocalVariableSymbol tmpCacheKey = block.DefineLocalVariable(_parent.ObjectArrayType, "~tmp~cache~key~{0}");
// Create a local variable that will hold the object that is returned from the caches Get method
LocalVariableSymbol objectFromCache = block.DefineLocalVariable(_parent.ObjectType, "~object~from~cache~{0}");
// Get the number of parameters sent to this method
int parameters = context.Method.Parameters.Count;
// Push the size of the array to the stack
context.InstructionWriter.EmitInstructionInt32(OpCodeNumber.Ldc_I4, parameters);
// Create a new array object
context.InstructionWriter.EmitInstructionType(OpCodeNumber.Newarr, _parent.ObjectType);
// Save the new array to the variable
context.InstructionWriter.EmitInstructionLocalVariable(OpCodeNumber.Stloc, tmpCacheKey);
//Loop all of the paramters for this method
for (int i = 0; i < context.Method.Parameters.Count; i++)
{
// Push the array onto the stack
context.InstructionWriter.EmitInstructionLocalVariable(OpCodeNumber.Ldloc, tmpCacheKey);
// Push the position index where you want to save the object in the array onto the stack
context.InstructionWriter.EmitInstructionInt32(OpCodeNumber.Ldc_I4, i);
// Push the value you want to save onto the stack
context.InstructionWriter.EmitInstructionInt32(OpCodeNumber.Ldarg_S, i);
// Copy everyting into the array
context.InstructionWriter.EmitInstruction(OpCodeNumber.Stelem_Ref);
}
// Push the array onto the stack as a parameter for the next method call
context.InstructionWriter.EmitInstructionLocalVariable(OpCodeNumber.Ldloc, tmpCacheKey);
// Call the GenerateKey method with the array as a parameter
context.InstructionWriter.EmitInstructionMethod(OpCodeNumber.Call, _parent.CacheGenerateKeyMethod);
// Copy the return value from GenerateKey to the variable
context.InstructionWriter.EmitInstructionLocalVariable(OpCodeNumber.Stloc, _cacheKeyVariable);
// Push the cache key onto the stack
context.InstructionWriter.EmitInstructionLocalVariable(OpCodeNumber.Ldloc, _cacheKeyVariable);
// Call the cache Get method with the cacke key as a parameter
context.InstructionWriter.EmitInstructionMethod(OpCodeNumber.Call, _parent.CacheGetMethod);
// Get the return value from the cache Get method
context.InstructionWriter.EmitInstructionLocalVariable(OpCodeNumber.Stloc, objectFromCache);
// Push the object we got from the cache method onto the stack
context.InstructionWriter.EmitInstructionLocalVariable(OpCodeNumber.Ldloc, objectFromCache);
// Push null to to stack
context.InstructionWriter.EmitInstruction(OpCodeNumber.Ldnull);
// Check if the value we got from the stack and null are equal
context.InstructionWriter.EmitInstruction(OpCodeNumber.Ceq);
// If they are equel we want to continue with the method as it's written
context.InstructionWriter.EmitBranchingInstruction(OpCodeNumber.Brtrue_S, endSequence);
// If they are not equal we want to return the value we got from the cache Get method back from the calling method
context.InstructionWriter.EmitInstructionLocalVariable(OpCodeNumber.Ldloc_S, objectFromCache);
context.InstructionWriter.EmitInstruction(OpCodeNumber.Ret);
context.InstructionWriter.DetachInstructionSequence();
// If we did not get anything back from the cahce do nothing and return to the method and continue executing
context.InstructionWriter.AttachInstructionSequence(endSequence);
context.InstructionWriter.EmitSymbolSequencePoint(SymbolSequencePoint.Hidden);
context.InstructionWriter.EmitInstruction(OpCodeNumber.Nop);
context.InstructionWriter.DetachInstructionSequence();
}
And then the WaveExit method, here we want to take the value that's returned form the method and put it into our cache using our generated cache key as the key,
private void WaveExit(WeavingContext context, InstructionBlock block)
{
//Create a instruction sequence
InstructionSequence entrySequence = context.Method.MethodBody.CreateInstructionSequence();
// Add the new instruction sequence at the end of the method body (but before the last return)
block.AddInstructionSequence(entrySequence, NodePosition.After, null);
// Attache the new instruction sequence to the current context
context.InstructionWriter.AttachInstructionSequence(entrySequence);
// Hide the code from debuggers
context.InstructionWriter.EmitSymbolSequencePoint(SymbolSequencePoint.Hidden);
// Push the cachekey we created in the WaveEntry method onto the stack
context.InstructionWriter.EmitInstructionLocalVariable(OpCodeNumber.Ldloc_S, _cacheKeyVariable);
// Push the return value onto the stack
context.InstructionWriter.EmitInstructionLocalVariable(OpCodeNumber.Ldloc_S, context.ReturnValueVariable);
// Call the cache Set method with the two parameters we have pushed onto the stack
context.InstructionWriter.EmitInstructionMethod(OpCodeNumber.Call, _parent.CacheSetMethod);
context.InstructionWriter.DetachInstructionSequence();
}
And to put it all to good use lets update our example program to look like this,
[Aspect.Cache]
private static int DoBigCalculation(string data)
{
int number;
if (int.TryParse(data, out number))
{
//This is just to simulate a big calculation/data operation you may have in your application
//that can be much fast with cache
System.Threading.Thread.Sleep(2000);
return number * 2;
}
return 0;
}
Now we have added a cache to the big calculation method and when you call it with the same argument again you should get the respnse much faster as its taken from the cache and not calculated again.
A couple of notes about the example program that I have included here, you have to copy the dll from the Weaver project to your bin directory and also the *.psplugin file, as there are not references to this project the files are not copied when you compile the application.
Example program for Part 2: CacheExample.zip (303.53 kb)