Posted on January 23, 2013 by Chris Harrington

Caching using aspect oriented programming with PostSharp

I’ve been hoping to add some caching to an application recently. It’s not a slow app, per se, but I figure that some caching is probably better than no caching and I’m expecting the app to grow, so it’d be nice to have the extra scalability available in the future.

My solution makes use of aspect oriented programming through PostSharp in C#. If you’re unfamiliar with AOP, the gist of it is that we’re modifying the behavior of methods before and after they’ve been called. In this case, I check a globally accessible cache before the method is executed to determine if I’ve got a cached result and, if so, return that instead of calling the method. After the method has been called, the result is stored in the cache so that the next time it’s called, we can use the stored value instead of executing the expensive method a second time.

The first step is to grab the PostSharp library.

install-package PostSharp

PostSharp works by providing access to a number of attributes (“aspects”) which we can use to decorate a particular class or method. We’re going to be using the OnMethodBoundaryAspect aspect and overriding the OnEntry and OnSuccess methods. The first will return a cached result if it exists and the second will store a result.

public override void OnEntry(MethodExecutionArgs args)
{
	if (args.Method.IsConstructor)
		return;

	var key = DeriveCacheKey(args);
	if (!Cache.ContainsKey(key))
		return;

	args.ReturnValue = Cache[key];
	args.FlowBehavior = FlowBehavior.Return;
}

Here, we’re building a key using the given arguments. As seen below, my key is generated using a concatenation of the class name, the method name and any arguments passed to the method. For greater fidelity, you could also add the namespace on to the beginning in the event you have classes with the same names and method names.

Because we want to return a cached value, we’re setting the FlowBehavior to Return, indicating that the method should not be executed.

The OnSuccess method is straightforward: assign the resulting value of a method to the cache. We have access to the same arguments as in the OnEntry method so we can generate the same key.

public override void OnSuccess(MethodExecutionArgs args)
{
	if (args.Method.IsConstructor)
		return;

	Cache.Store(DeriveCacheKey(args), args.ReturnValue);
}

To use the caching, decorate the method (or class for every method in that class) with the CacheAttribute attribute and away you go.

[Cache]
public IEnumerable GetAllUsers()
{
    ...
}

And that’s it! The code above references some properties and helper methods; I’ve pasted the entirety of the class below for reference. One thing to note is that you’ll need some sort of static class to hold the cached data. I’m using the ICache interface which basically works as a limited dictionary. I’m retrieving the implementation of this interface from a dependency container, but you could easily replace this with a static class that handles all of the caching application-wide.

[Serializable]
public class CacheAttribute : OnMethodBoundaryAspect
{
	#region Data Members
	private ICache _cache;
	#endregion

	#region Properties
	public ICache Cache
	{
		get { return _cache ?? (_cache = DependencyResolver.Current.GetService()); }
	}
	#endregion

	#region Public Methods
	public override void OnEntry(MethodExecutionArgs args)
	{
		if (args.Method.IsConstructor)
			return;

		var key = DeriveCacheKey(args);
		if (!Cache.ContainsKey(key))
			return;

		args.ReturnValue = Cache[key];
		args.FlowBehavior = FlowBehavior.Return;
	}

	public override void OnSuccess(MethodExecutionArgs args)
	{
		if (args.Method.IsConstructor)
			return;

		Cache.Store(DeriveCacheKey(args), args.ReturnValue);
	}
	#endregion

	#region Private Methods
	private static string DeriveCacheKey(MethodExecutionArgs args)
	{
		return args.Method.DeclaringType.Name + "-" + args.Method.Name + (args.Arguments.Any() ? "-" + args.Arguments.Aggregate((first, second) => first + "-" + second) : "");
	}
	#endregion