Declarative Transactions Sample

Open this sample in Visual Studio

The PostSharp.Samples.Cache project demonstrates the use of PostSharp Laos to author a custom attribute that caches the results of the method to which it is applied. It supposes that the method is deterministic on its parameters, that is, it will always return the same value when the same parameters are provided.

Like most of samples, it is rather a proof-of-concept than a production-ready implementation. 

This sample is written in Visual J#.

Implementation

Implementing caching may be very easy. Basically, we check the cache state before executing the method, return immediately if the value is already present in the cache, or continue with method execution otherwise. At the end of the method, we store the item in the cache.

In order to add some logic at the beginning and at the end of methods, we can use the OnMethodBoundary aspect. Since we want our aspect to be usable as a custom attribute, we will derive the OnMethodBoundaryAspect class. We will override the OnEntry and OnSuccess methods.

/*  @attribute Serializable()  
* @attribute AttributeUsage( AttributeTargets.Method ) */
public final class CacheAttribute extends OnMethodBoundaryAspect
{

public void OnEntry(MethodExecutionEventArgs eventArgs)
{
}

public void OnSuccess(MethodExecutionEventArgs eventArgs)
{
}

}

The Cache Backend

We have to cache the method return value somewhere. In this sample, for simplicity, we chose a dictionary. It is of course possible to use cache implementation.

static Dictionary<String,Object> cache;

The Cache Key

At method entry, we compute a cache key from the method name and the given parameters. This cache key uniquely identifies the piece of information provided by this method. In this sample, we chose to use a string composed of the method name and the string representation of its parameters (using the ToString method). 

Other approaches, like relying on GetHashCode and Equal, are of course possible.

But let's continue with a string representation. We use a helper class named Formatter to compute formatting strings that represent methods. This formatting string represents placeholders for parameters and type parameters. Note that the formatting string in itself does not vary at runtime and is relatively expensive to compose. This is a good reason to compose it at compile-time and to serialize it into the aspect instance, so that it doesn't have to be computed again at runtime.

The formatting string and its runtime logic are encapsulated in a class named MethodFormatStrings. For the implementation of MethodFormatStrings and Formatter, which is quite uninteresting, please refer to the source code.

MethodFormatStrings formatStrings;

public void CompileTimeInitialize(MethodBase method)
{
this.formatStrings = Formatter.GetMethodFormatStrings(method);
}

Cache Lookup

In order to implement caching on an existing method, we should first, at method entry, look in the cache whether an item with the computed cache key already exists. If yes, we can return immediately this value. We set to properties of the aspect event arguments: ReturnValue, obviously, to the value found in cache, and FlowBehavior to Return.

Otherwise, we should continue the method execution. Since we will need the cache key on method exit as well (when we will store the method value in cache), we will store it in the MethodExecutionTag. It is a property of the aspect event arguments whose value is preserved across methods OnEntry, OnExit, OnSuccess and OnException.

public void OnEntry(MethodExecutionEventArgs eventArgs)
{
// Compose the cache key.
String key = this.formatStrings.Format(
eventArgs.get_Instance(), eventArgs.get_Method(), eventArgs.GetArguments());

// Test whether the cache contains the current method call.
if (!cache.ContainsKey(key))
{
// If not, we will continue the execution as normally.
// We store the key in a state variable to have it in the OnExit method.
eventArgs.set_MethodExecutionTag(key);
}
else
{
// If it is in cache, we set the cached value as the return value
// and we force the method to return immediately.
eventArgs.set_ReturnValue(cache.get_Item(key));
eventArgs.set_FlowBehavior(FlowBehavior.Return);
}
}

Cache Storage

If want to store the method return value in cache, we have to implement the OnSuccess method of OnMethodBoundaryAspect. As its name indicates, this method is invoked only when the aspected method is successful.

We retrieve the cache key from the MethodExecutionTag of the aspect event arguments, where we have stored in in the OnEntry method.

public void OnSuccess(MethodExecutionEventArgs eventArgs)
{
// Retrieve the key that has been computed in OnEntry.
String key = (String)eventArgs.get_MethodExecutionTag();

// Put the return value in the cache.
cache.set_Item(key, eventArgs.get_ReturnValue());
}

Validation of the custom attribute usage

There is still one important thing to do: validate, at compile-time, that our Cache custom attribute has been applied to a method where it makes sense.

Clearly, it does not make sense to apply this custom attribute to constructors or to methods whose return type is void. Additionally, note that we do not support parameters passed by reference (out or ref in C#). It is not a limitation of PostSharp Laos but just a consequence of our laziness in this example.

Compile-time validation is performed by the CompileTimeValidate method. In order to emit error messages, we have to create a resource file containing error messages and create an instance of MessageSource.

public boolean CompileTimeValidate(MethodBase method)
{
// Don't apply to constructors.
if (method instanceof ConstructorInfo)
{
CacheMessageSource.Instance.Write(SeverityType.Error, "CX0001", null);
return false;
}

MethodInfo methodInfo = (MethodInfo)method;

// Don't apply to void methods.
if (methodInfo.get_ReturnType().get_Name() == "Void")
{
CacheMessageSource.Instance.Write(SeverityType.Error, "CX0002", null);
return false;
}

// Does not support out parameters.
ParameterInfo[] parameters = method.GetParameters();
for (int i = 0; i < parameters.length; i++)
{
if (parameters[i].get_IsOut())
{
CacheMessageSource.Instance.Write(SeverityType.Error, "CX0003", null);
return false;
}
}

return true;
}

Using the Cache custom attribute

Now we can create a small program that demonstrates our Cache custom attribute.
public class Program
{
public static void main(String[] args)
{
System.out.println("1 ->" + GetDifficultResult(1));
System.out.println("2 ->" + GetDifficultResult(2));
System.out.println("1 ->" + GetDifficultResult(1));
System.out.println("2 ->" + GetDifficultResult(2));
}

/** @attribute Cache() **/
static int GetDifficultResult(int arg)
{
// If the following text is printed, the method was not cached.
System.out.println("Some difficult work!");
return arg;
}
}
Note that the "Some difficult work" text is printed only the first time that the method is called with that argument.