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#.
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)
{
}
}
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;
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);
}
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);
}
}
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());
}
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;
}
public class ProgramNote that the "Some difficult work" text is printed only the first time that the method is called with that argument.
{
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;
}
}