Caching method results easily with AOP

In this post, I'll show you how to add caching functionality with Aspect Oriented Programming to your C# applications just by adding a method attribute.

This post was originally published in 2015 and may contain outdated information.

In many cases, using a caching mechanism can drastically improve performances. Processed results of methods can be stored in memory or in files so they are ready to be returned immediately when we call the methods again.

In some .NET applications, we can use the MemoryCache class from System.Runtime.Caching to store data in memory. However, using such library in a basic way may induce some problems such as:

  • Adding caching in each method which needs it may result in less readable and polluted code.
  • The repetitive use of such code may induce errors (especially copy-pasted code).
  • Depending on the nature of projects (mobile apps, websites, web services, desktop apps), we may need a different caching approach.

In order to avoid all these problems, I have written a generic caching aspect using Autofac and Castle DynamicProxy.

Demo

Caching the result of a method is as simple as the following code:

[CacheResult(Duration = 1000)]
public string Process(string arg)
{
    return Guid.NewGuid().ToString("N");
}

Here, we call the method many times but before the last call we do a 2 seconds break:

Console.WriteLine(worker.Process("foo"));
Console.WriteLine(worker.Process("foo"));
Console.WriteLine(worker.Process("foo"));

Console.WriteLine("Sleeping 2 seconds...");
Thread.Sleep(2000);

Console.WriteLine(worker.Process("foo"));

// result
// b7d719b45a01469797c974164179b4b5
// b7d719b45a01469797c974164179b4b5
// b7d719b45a01469797c974164179b4b5
// Sleeping 2 seconds...
// f2ad87faf4fa4daa98ad1fc3e87fde37

As expected, the three first calls returned the same Guid instead of three different because the first call result were cached for one second. After the two seconds break, another result were processed for the last call as the first value has expired.

Implementing the caching aspect

First, we have to create a caching abstraction using an interface. Depending on the project’s nature, the implementation will be different. In my case, I will use the MemoryCache default singleton which is good for desktop applications:

public interface ICacheProvider
{
    object Get(string key);

    void Put(string key, object value, int duration);

    bool Contains(string key);
}

public class MemoryCacheProvider : ICacheProvider
{
    public object Get(string key)
    {
        return MemoryCache.Default[key];
    }

    public void Put(string key, object value, int duration)
    {
        if (duration <= 0)
            throw new ArgumentException("Duration cannot be less or equal to zero", "duration");
        
        var policy = new CacheItemPolicy
        {
            AbsoluteExpiration = DateTime.Now.AddMilliseconds(duration)
        };

        MemoryCache.Default.Set(key, value, policy);
    }

    public bool Contains(string key)
    {
        return MemoryCache.Default[key] != null;
    }
}

Secondly, we have to create the CacheResult attribute:

public class CacheResultAttribute : Attribute
{
    public int Duration { get; set; }
}

In a third time, we need to implement the class that will intercept the methods decorated with the CacheResult attribute. For each method call, we will compute a unique key based on the class name, the method name and the parameters. If the result exists in the cache provider, we will return it as is. Otherwise, the method will be executed and the result will be stored in the cache with the specified duration in milliseconds:

public class CacheResultInterceptor : IInterceptor
{
    private readonly ICacheProvider _cache;

    public CacheResultInterceptor(ICacheProvider cache)
    {
        _cache = cache;
    }

    public CacheResultAttribute GetCacheResultAttribute(IInvocation invocation)
    {
        return Attribute.GetCustomAttribute(
            invocation.MethodInvocationTarget,
            typeof(CacheResultAttribute)
        )
        as CacheResultAttribute;
    }

    public string GetInvocationSignature(IInvocation invocation)
    {
        return String.Format("{0}-{1}-{2}",
            invocation.TargetType.FullName,
            invocation.Method.Name,
            String.Join("-", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray())
        );
    }

    public void Intercept(IInvocation invocation)
    {
        var cacheAttr = GetCacheResultAttribute(invocation);

        if (cacheAttr == null)
        {
            invocation.Proceed();
            return;
        }

        string key = GetInvocationSignature(invocation);

        if (_cache.Contains(key))
        {
            invocation.ReturnValue = _cache.Get(key);
            return;
        }

        invocation.Proceed();
        var result = invocation.ReturnValue;

        if (result != null)
        {
            _cache.Put(key, result, cacheAttr.Duration);
        }
    }
}

Finally, we need to wire all these classes with an Autofac module:

public class MyModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterType<MemoryCacheProvider>()
            .As<ICacheProvider>()
            .SingleInstance();

        builder.RegisterType<CacheResultInterceptor>()
            .SingleInstance();

        builder.RegisterType<FakeExpensiveClass>()
            .As<IDoExpensiveWork>()
            .EnableInterfaceInterceptors()
            .InterceptedBy(typeof(CacheResultInterceptor))
            .SingleInstance();
    }
}
Licensed under CC BY 4.0
Ko-fi donations Buy me a coffee