Bernie Cook's Blog

Azure, C#, .NET, Architecture & Related Tech News

Repository Caching with Aspect Oriented Programming

5 Comments

Repository Caching with Aspect Oriented ProgrammingThis post provides a walkthrough of how to implement basic cache handling at the repository layer using Aspect Oriented Programming (AOP). I’ve chosen PostSharp’s AOP framework and built a relatively simple Domain-Driven Design (DDD) solution in .NET to illustrate how it all ties together.

I want delve into too much AOP detail up front other than to reiterate the usual AOP pitch – that it’s an incredibly useful programming paradigm which addresses cross-cutting coding concerns within application development. What does that mean in English? Once you’ve mastered the basic AOP terminology and implementation requirements you can use it to remove a lot of duplicate code that appears throughout your solution. If you’re not familiar with AOP then take a quick moment to watch one of PostSharp’s many introductory screencasts or browse through their documentation. Keep in mind that AOP isn’t OOP as you work your way through the following code, it complements OOP extremely well but doesn’t follow object-oriented principles.

I’ll also make mention of several key AOP terms throughout the post, providing samples before delving into the terminology.

Source Code & Database

The source code for this solution is available on GitHub. The solution relies on the Northwind database (.zip, 524KB) so please ensure you have a copy handy and also ensure you’ve updated the web.config’s “NorthwindContext” connection string.

Solution Overview

The solution consists of several projects:
Solution Explorer
So basically what you have is a relatively simple C# ASP.NET MVC DDD solution. The Controllers talk to the Services, the Services talk to the Repositories, and the Repositories talk to the backing store – which in this case is the Northwind database (see the Entity Framework diagram below). And POCO classes have been generated by a T4 template.
Northwind Entity Framework Diagram
The solution consists of several web pages; a home page and 3 sections – with each providing a basic browse, edit details and a success page. The screenshots below illustrate the pages for the Products entity.
Product Listing Product Edit Details Product Successful
The cache worker role was chosen to test the caching in a distributed environment, swapping it out with another is quite simple and I’ll cover that later in the post.

Repository Caching

The objective behind the design and implementation of this basic AOP/repository cache solution was:

  1. Cache any data returned from repository get calls
  2. Remove any cache entities if a related entity was inserted, updated or deleted

To further clarify – if my service did a get on a collection of Northwind products, or even a single product, I wanted the results cached for any subsequent calls. If I inserted a new product, updated an existing one, or deleted a product, then I wanted to remove any related products from cache.

In addition I wanted to be able to cache both:

  1. generic repositories, and
  2. non-generic repositories

There are 2 generic repositories in total (Northwind’s Product and Supplier), and 1 non-generic repository (Northwind’s Customer).

So lets look at some code, starting with the generic repository interface.

IGenericRepository.cs

using System;
using System.Collections.Generic;
using System.Linq.Expressions;

namespace CacheAspect.Repository
{
    public interface IGenericRepository<TEntity> where TEntity : class
    {
        TEntity GetById(object id);
        IEnumerable<TEntity> GetAll();
        IEnumerable<TEntity> GetAll(Expression<Func<TEntity, bool>> filter);

        void Insert(TEntity entity);

        void Update(TEntity entity);

        void Delete(TEntity entity);
    }
}

So nothing out of the ordinary. I could have combined the GetAll() methods  into one by making “filter” an optional parameter but I purposefully kept them separate for reasons I’ll cover later in the post.

Now for the implementation, take note of the attributes – or as they’re more correctly called in the AOP world; aspects:

GenericRepository.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Data.Entity;
using System.Linq.Expressions;
using CacheAspect.DomainModel;

namespace CacheAspect.Repository
{
    public class GenericRepository<TEntity> : IGenericRepository where TEntity : class
    {
        public readonly NorthwindContext Context;
        public readonly DbSet DbSet;

        public GenericRepository(NorthwindContext context)
        {
            Context = context;
            DbSet = context.Set<TEntity>();
        }

        [Cache(CacheAction.Add)]
        public TEntity GetById(object id)
        {
            return DbSet.Find(id);
        }

        [Cache(CacheAction.Add)]
        public IEnumerable GetAll()
        {
            IQueryable query = DbSet;

            return query.ToList();
        }

        public IEnumerable GetAll(
            Expression<Func<TEntity, bool>> filter)
        {
            IQueryable query = DbSet;

            if (filter != null)
            {
                query = query.Where(filter);
            }

            return query.ToList();
        }

        [Cache(CacheAction.Remove)]
        public void Insert(TEntity entity)
        {
            DbSet.Add(entity);
        }

        [Cache(CacheAction.Remove)]
        public void Update(TEntity entity)
        {
            DbSet.Attach(entity);

            Context.Entry(entity).State = EntityState.Modified;
        }

        [Cache(CacheAction.Remove)]
        public void Delete(TEntity entity)
        {
            if (Context.Entry(entity).State == EntityState.Detached)
            {
                DbSet.Attach(entity);
            }

            DbSet.Remove(entity);
        }
    }
}

So the attributes – referred to as “aspects” in AOP – are loosely coupled, modularised units that are executed when control passes to the designated methods. Aspects are separated from the base program, and can be used across a base program to address horizontal requirements. This particular AOP caching aspect [Cache(…)] addresses a limited cross-cutting concern and if you think about how you could write an aspect to address solution-wide exception handling, or validation, then you can see how far reaching it can be. I’m starting to delve into too much theory so lets return back to the land of examples.

All bar one of the above methods has an aspect applied, and as you can see each [Cache] aspect accepts a parameter indicating that it should add or remove a cache entity accordingly – [Cache(CacheAction.Add)] and [Cache(CacheAction.Remove)]

I wanted to test my cache aspect against a non-generic repository so I created a less generic one for Northwind Customers entity, shown below:

CustomerRepository.cs

using System.Collections.Generic;
using CacheAspect.DomainModel;

namespace CacheAspect.Repository
{
    public class CustomerRepository : GenericRepository, ICustomerRepository
    {
        public CustomerRepository(NorthwindContext context)
            : base(context)
        {
        }

        [Cache(CacheAction.Add)]
        public IEnumerable GetAllByContactTitle(string contactTitle)
        {
            return GetAll(q => q.ContactTitle.Contains(contactTitle));
        }
    }
}

The GetAllByContactTitle(contactTitle) method above also has a [Cache] attribute, which in turn calls the generic GetAll(filter) which doesn’t have a [Cache] attribute. This was done intentionally as there is no point doubling up on caching, any calls would get cached at GetAllByContactTitle(contactTitle) before GetAll(filter).

So what does this provide? It allows get results from the repository layer to be cached [Cache(CacheAction.Add)] and invalidates any cache items for that same repository when any methods flagged with [Cache(CacheAction.Remove)] are called.

Now lets look at the cache aspect.

AOP Cache Aspect

The code that runs the cache aspect is contained in the following class:

CacheAttribute.cs

using System;
using System.Linq;
using System.Reflection;
using System.Text;
using PostSharp.Aspects;
using PostSharp.Extensibility;

namespace CacheAspect.Repository
{
    /// <summary>
    /// Cache attribute.
    /// </summary>
    [Serializable]
    public class CacheAttribute : MethodInterceptionAspect
    {
        [NonSerialized]
        private static readonly ICache Cache;

        [NonSerialized]
        private object _syncRoot;
        private string _methodName;
        public readonly CacheAction Action;

        /// <summary>
        /// Static constructor.
        /// </summary>
        static CacheAttribute()
        {
            if (!PostSharpEnvironment.IsPostSharpRunning)
            {
                Cache = new AzureCache();
            }
        }

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="action">Cache action.</param>
        public CacheAttribute(CacheAction action)
        {
            Action = action;
        }

        /// <summary>
        /// Initialisation during compile time.
        /// </summary>
        /// <param name="methodBase">Method information.</param>
        /// <param name="aspectInfo">Aspect information.</param>
        /// <remarks>
        /// Normally you'd determine the class name at this point but because we're using generic repositories it's of no use.
        /// </remarks>
        public override void CompileTimeInitialize(MethodBase methodBase, AspectInfo aspectInfo)
        {
            _methodName = methodBase.Name;
        }

        /// <summary>
        /// Runtime initialisation.
        /// </summary>
        /// <param name="methodBase">Method information.</param>
        public override void RuntimeInitialize(MethodBase methodBase)
        {
            _syncRoot = new object();
        }

        /// <summary>
        /// Invoke on method call.
        /// </summary>
        ///methodInterceptionArgs">
        public override void OnInvoke(MethodInterceptionArgs methodInterceptionArgs)
        {
            if (Action == CacheAction.Add)
            {
                var cacheKey = BuildCacheKey(methodInterceptionArgs);

                if (Cache[cacheKey] != null)
                {
                    methodInterceptionArgs.ReturnValue = Cache[cacheKey];
                }
                else
                {
                    lock (_syncRoot)
                    {
                        if (Cache[cacheKey] == null)
                        {
                            var returnVal = methodInterceptionArgs.Invoke(methodInterceptionArgs.Arguments);
                            methodInterceptionArgs.ReturnValue = returnVal;

                            Cache[cacheKey] = returnVal;
                        }
                        else
                        {
                            methodInterceptionArgs.ReturnValue = Cache[cacheKey];
                        }
                    }
                }
            }
            else
            {
                var typeName = GetTypeName(methodInterceptionArgs.Binding.GetType());

                lock (_syncRoot)
                {
                    Cache.Remove(typeName);
                }

                methodInterceptionArgs.ReturnValue = methodInterceptionArgs.Invoke(methodInterceptionArgs.Arguments);
            }
        }

        /// <summary>
        /// Build the cache key using the type name, method name and parameter argument values.
        /// </summary>
        /// <param name="methodInterceptionArgs">Aspect arguments.</param>
        /// <returns>Cache key.</returns>
        private string BuildCacheKey(MethodInterceptionArgs methodInterceptionArgs)
        {
            const string divider = "_";

            var typeName = GetTypeName(methodInterceptionArgs.Binding.GetType());

            var cacheKey = new StringBuilder();
            cacheKey.Append(typeName);
            cacheKey.Append(divider);
            cacheKey.Append(_methodName);

            foreach (var argument in methodInterceptionArgs.Arguments.ToArray())
            {
                cacheKey.Append(argument == null ? divider : argument.ToString());
            }

            return cacheKey.ToString();
        }

        /// <summary>
        /// Use reflection to get the object's type name.
        /// </summary>
        /// <param name="type">The object's type.</param>
        /// <returns>Type name.</returns>
        /// <remarks>
        /// If we're supporting non-generic repositories we need to identify the correct type name.
        /// </remarks>
        private string GetTypeName(Type type)
        {
            return ((type.UnderlyingSystemType).GenericTypeArguments.Any())
                ? ((type.UnderlyingSystemType).GenericTypeArguments[0]).Name
                : type.DeclaringType.Name;
        }
    }

    /// <summary>
    /// Cache action types.
    /// </summary>
    public enum CacheAction
    {
        /// <summary>
        /// Add a new item to cache.
        /// </summary>
        Add,
        /// <summary>
        /// Remove all associated items from cache for the given domain model.
        /// </summary>
        Remove
    }
}

There is a bit of code here but it’s actually quite simple once it’s broken down. I’ll start with how it adds entities to the cache and then move on to how it removes them.

Adding an Entity to Cache

The “CacheAttribute” class is an aspect. It inherits the MethodInterceptionAspect which is a PostSharp abstract class that helps aspects intercept invocations of any method that the aspect is applied to. So placing [Cache(…)] to a repository method means that our aspect can catch all calls made to that method and then perform a given action(s) – in our case it means caching the results.

I’ll walk through the sequence of events which occur when the ProductService makes a get call to the repository.

  1. The ProductService calls GetById(id)
  2. CacheAttribute, our aspect, intercepts the call and passes control to it’s Invoke(methodInterceptionArgs) method
  3. Invoke(…) then…
    1. Checks the cache action enumeration type, in our case it’s an “Add
    2. Constructs a cache key – which is a string that combines the entity type, method name and parameter values. For example, the service wants a product with the id of “5” so the resulting cache key is: “Product_GetById5
    3. Checks the cache store for a matching key, one isn’t found because we haven’t added it yet, and then Invoke() calls the intercepted method, in our case it’s GetById(id)
  4. The repository’s GetById(id) then executes and returns the product entity
  5. Invoke(…) then …
    1. Stores the return value in the cache store against the cache key
    2. Sets the intercepted method’s return value
  6. And control returns to the ProductService

Worth pointing out is that the BuildCacheKey() retrieves the underlying, or declared, type for the generic or non-generic repositories and prefixes it to the cache key. This keeps our cache key unique across all repositories.

Before I proceed I want to cover three AOP terms, these are fundamental to understanding the AOP framework so deserve some attention:

  • Join point – A point in the code where behaviours are joined, such as when we joined a method in our base code to the aspect. Generally speaking join points are simply points in the control flow of a program. In AOP, where aspects are placed, these join points are called pointcuts.
  • Pointcut – An AOP join point. When a pointcut is reached control is passed to the aspect.
  • Advice – The method invoked when we reach a pointcut

So if we look at the above example when the control flow of the program reaches the more generally known join point, which is a method that has an aspect applied as an attribute (also referred to more s specifically in AOP as a pointcut) it passes control through to the advice. Got it? Great.

Removing an Entity from Cache

Removing an entity from cache is quite straightforward.

  1. The ProductService calls Update(product)
  2. The aspect applied to the Update() passes control to the Invoke(methodInterceptionArgs) method
  3. Invoke(…) then…
    1. Checks the action enumeration type , in our case it’s a “Remove
    2. Retrieves the type name, in this case it’s “Product
    3. Cache.Remove() is called with the type name
  4. Cache, in this case the AzureCache object, iterates through all of the cache keys and removes those prefixed with “Product”
  5. Control is returned back up to ProductService and the data store update is performed.

If you run the application and view the product listing and a few products without making changes you’ll soon build up quite a list of cache entities prefixed with “Product” so it’s these that are removed we the update is called.

At this point you can probably understand the inner workings, and have grasped the potential of this approach and the chosen technologies. In addition, you’ll probably see a few limitations that this basic sample doesn’t address if you were to prepare it for a production environment. Worth noting is that you can actually pass an Expression to the cache aspect as a method parameter (see the GetAll(filter) method) and it will have no trouble creating a cache key from this as well, albeit a longer cache key.

Hopefully though the point of this post is clear, in that we’ve used AOP to create a basic caching model at the repository layer.

One final thing before I tie things up – here is a copy of the AzureCache.cs:

AzureCache.cs

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.ApplicationServer.Caching;

namespace CacheAspect.Repository
{
    public class AzureCache : ICache
    {
        private const string CacheKeys = "CacheKeys";
        private const string DataCacheName = "default";

        private readonly TimeSpan _defaultTimeout;
        private readonly DataCache _dataCache;

        /// <summary>
        /// Constructor.
        /// </summary>
        public AzureCache()
        {
            _defaultTimeout = new TimeSpan(1, 0, 0);

            _dataCache = new DataCache(DataCacheName);
        }

        /// <summary>
        /// Cache indexer.
        /// </summary>
        /// <param name="key">Cache key.</param>
        /// <returns>Cache value.</returns>
        public object this[string key]
        {
            get
            {
                return _dataCache.Get(key);
            }
            set
            {
                UpdateCacheKeys(key);

                _dataCache.Put(key, value, _defaultTimeout);
            }
        }

        /// <summary>
        /// Update the cache keys list.
        /// </summary>
        /// <param name="key">New key to add to the cache key list, if it doesn't already exist.</param>
        private void UpdateCacheKeys(string key)
        {
            if (key.Equals(CacheKeys))
            {
                throw new ArgumentException("The current cache key cannot be used as it's already employed by the system.", "key");
            }

            IList cacheKeys;
            var cacheKeysObject = _dataCache.Get(CacheKeys);

            if (cacheKeysObject == null)
            {
                cacheKeys = new List();
            }
            else
            {
                cacheKeys = (List)cacheKeysObject;

                if (cacheKeys.Contains(key))
                {
                    return;
                }
            }

            cacheKeys.Add(key);
            _dataCache.Put(CacheKeys, cacheKeys, _defaultTimeout);
        }

        /// <summary>
        /// Remove the cache key, and any related keys, from cache.
        /// </summary>
        /// <param name="key">Cache key.</param>
        public void Remove(string key)
        {
            var cacheKeysObject = _dataCache.Get(CacheKeys);

            if (cacheKeysObject == null)
            {
                return;
            }

            var cacheKeys = (List)cacheKeysObject;

            foreach (var cacheKey in cacheKeys.Where(c => c.StartsWith(key)).ToList())
            {
                _dataCache.Remove(cacheKey);
                cacheKeys.Remove(cacheKey);
            }

            _dataCache.Put(CacheKeys, cacheKeys, _defaultTimeout);
        }
    }
}

MSIL Modification

Important to note is that PostSharp modifies the MSIL code after the standard compiler has done it’s work. It is this modification which allows you to use AOP in your .NET solution. This is also the reason for the CompileTimeInitialize() method a the top of the CacheAttibute class. Further reading on MSIL Injection can be found on the PostSharp website.

Non-Azure Caching

Adopting a different cache store is quite simple. As long as you implement ICache.cs and alter the static CacheAttribute constructor you’ll be good to go.

I’ve left a slightly modified version of Matthew Groves’ StaticMemoryCache.cs in the Repository project (direct GitHub link to the modified version) so feel free to use that if you want to run the web role on a single instance.

Limitations

There are a quite few architectural changes I’d make to this solution if I were to use it in a production environment – upgrading how the cache entities are handled in the event of nesting, smarter cache updating and insertions for collections, better encapsulation, more attention to cohesion and coupling etc. This solution was designed more as an educational example so do excuse it’s shortcomings.

Acknowledgement

The idea behind this post was sparked by some code written by Matthew Groves. I must point out that if you’re interested in seeing a number of PostSharp examples he has a great PostSharp repository on GitHub.

Advertisements

Author: Bernie

I currently live and work in the UK, just outside of London. I've been working in IT for 15+ years and in that time have solved many technical problems via blogs, forums, tutorials etc. and feel like I should be giving something back. This blog post is my way of contributing and I hope it proves just as useful to others as their contributions have to me.

5 thoughts on “Repository Caching with Aspect Oriented Programming

  1. A very nice example of a real-world caching aspect.

    However, I must disagree that your use of AOP goes against the Single Responsiblity Principle! Your aspect is one class that has one responsibility (caching). Your repository is one class that has one responsibility (data access). Each class has one (and only one) reason to change.

    Furthermore, your use of the ICache interface represents another good object-oriented principle (dependency inversion). You aspect depends on an interface, not a specific implementation (line 31 of CacheAttribute.cs not withstanding 🙂

    • Hi Matt, thanks for your feedback! It’s very much appreciated.

      I re-read the SRP reference you mentioned and am in complete agreement. I must have missed that on the proof read, and have since taken it out.

      On the DI front – again I’m in agreement. I was going to wire up Ninject to the CacheAttribute constructor but wanted to keep a reign on the level of detail contained within the example. As much as I wanted to make the solution production-worthy I knew doing so would increase the complexity and slightly cloud the purpose of this particular post.

      All the best for your talk on the 20th.

  2. Can you do a followup one day with a production-worthy version?

    • I would love to but time isn’t on my side. My recommendation would be to work on a sample project such as this and add a number of production requirements to it to see how it stands up.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s