Caching with Entity Framework

As I said before in my post about the repository pattern, entity framework doesn’t come with any caching.  I’m pretty sure this will be added at some point, and I look forward to not worrying about it myself…  After all, caching static data is quite a common scenario.  One thing to remember is that the ObjectContext does some caching for you, but it’s no good for you if you want to cache data for a long time or across multiple contexts.  This post shows my approach to data caching.  It extends the caching aspect, which you can read more about here.  If you’re not doing AOP and are still interested in caching then this is still relevant (at least I hope it is…) 

Before I start talking about the implementation, let’s think about the requirements…  We want to cache the result of a query (i.e. give me the currency USD).  When someone re-runs the same query (give me the currency USD) they are given the same result (we don’t go to the DB again for it).  When someone runs a different query (give me the currency GBP) they are not given the wrong result (i.e. USD).  It must be thread safe.  It would also be good to be able to say that I only want to cache the result of the query for the duration of the session / unit of work.  It would also be cool if you could say, cache this for 2 minutes or so (this was already done with caching aspect, so I just need to make sure it works OK with entities)…

There’s also some restrictions which Entity Framework (and other ORM’s to be fair) bring to the table.  The most noticeable are around attaching / detaching.  The container only knows about an object if it’s attached (if you do payment.Currency = cachedCurrency; and it’s not attached the container will think it’s new, and insert it…).  Also, an object instance can only be attached to a single context at a time.  This means that we need to be able to clone instances and automatically attach them to the container (assuming they aren’t already attached – remember, not all objects fetched are cached and not all cached objects are from a different container)…

I’ve kept the cache as simple as possible to meet my requirements…  There are some things which I’ve left out, like purging items when they change (I’ll add it when I find out I need it)…  I also only cache the results of a query which returns a single result (T Find<T>(…)), you can’t cache a IQuerable<T> FindAll<T>(…).  Again, I haven’t needed that yet, and it could means that we cache a lot more data than we may need (for example, the most common currencies you get are USD and GBP, so why cache the 100 odd other ones until you actually need them?)

Let’s start off with configuring caching:

container.Register(RepositoryComponent
    .For<ISomeEntityRepository, SomeEntity>()
    .ImplementedBy<SomeEntityRepository>()
    .CacheFindSingle<GetSomeEntityBySomeProperty>(k => k.PropertyName).ExpireAfterSession());

The thing to notice is that when we ask for caching to be enabled we tell it the specification and how to generate a cache key from that specification (I could rely on Equals() of the spec, or get it to implement a ICacheKey or something, but I think this approach means it can be configured externally).

As I said before, the caching is applied as an aspect (extending the existing caching aspect):

public class SessionCacheAspectAdapter : Cache, IAspectMethodCache
 {
     private readonly IServiceLocator serviceLocator;
     private readonly ICloneGenerator cloneGenerator;
     private CacheMethodSettings cacheMethodSettings;
 
     public SessionCacheAspectAdapter(IServiceLocator serviceLocator, ICacheStore cacheStore, ICloneGenerator cloneGenerator, ICachedItemExpirationMonitor expirationMonitor)
         : base(cacheStore, expirationMonitor, new ICacheRestriction[0])
     {
         if (serviceLocator == null) throw new ArgumentNullException("serviceLocator");
         if (cacheStore == null) throw new ArgumentNullException("cacheStore");
         if (cloneGenerator == null) throw new ArgumentNullException("cloneGenerator");
         this.serviceLocator = serviceLocator;
         this.cloneGenerator = cloneGenerator;
     }
 
     private ILinqRepository Repository
     {
         get
         {
             if (cacheMethodSettings == null)
                 throw new ArgumentNullException("cacheMethodSettings", "There are no cache method settings - Configure should have been called before we try and access any cache items");
 
             return (ILinqRepository)serviceLocator.Resolve(cacheMethodSettings.CacheTypeSettings.ServiceType);
         }
     }
 
     private IQueryCache QueryCache
     {
         get
         {
             return Repository.Session.Cache;
         }
     }
 
     public override bool TryGetValue(KeyValuePair<string, object>[] key, out object item)
     {
         if (QueryCacheConfiguration.ShouldCacheForSession(cacheMethodSettings))
         {
             return QueryCache.TryGetValue(key, out item);
         }
 
         var result = base.TryGetValue(key, out item);
 
         if (result)
         {
             var repository = Repository;
             var attachedItem = repository.GetAttachedItem(item);
             if (attachedItem != null)
             {
                 item = attachedItem;
             }
             else
             {
                 item = cloneGenerator.Clone(item);
                 AttachItemToCurrentRepositry(item, repository);
             }
         }
 
         return result;
     }
 
     private static void AttachItemToCurrentRepositry(object item, ILinqRepository repository)
     {
         var entity = item as EntityObject;
         if (entity != null)
         {
             repository.Attach(entity);
             return;
         }
         throw new InvalidCastException("cached item must be a EntityObject");
     }
 
     public override void AddOrReplace(KeyValuePair<string, object>[] key, object item)
     {
         if (!ShouldCache(item)) return;
         if (QueryCacheConfiguration.ShouldCacheForSession(cacheMethodSettings))
         {
             QueryCache.AddOrReplace(key, item);
             return;
         }
         var settings = new CacheExpirySettings();
         settings.NeverExpire();
         CacheInStore(key, item, settings);
     }
 
     public override void AddOrReplace(KeyValuePair<string, object>[] key, object item, CacheExpirySettings expirySettings)
     {
         if (!ShouldCache(item)) return;
         if (QueryCacheConfiguration.ShouldCacheForSession(cacheMethodSettings))
         {
             if (expirySettings.ExpiryOption == CacheExpiryOption.Never)
             {
                 QueryCache.AddOrReplace(key, item);
                 return;
             }
             throw new NotSupportedException(
                 "Items cannot be cached in the session with expiry settings - they live as long as ther session, so that's how they expire...");
         }
 
         CacheInStore(key, item, expirySettings);
     }
 
     private void CacheInStore(KeyValuePair<string, object>[] key, object item, CacheExpirySettings expirySettings)
     {
         if (!(item is EntityObject))
             throw new NotSupportedException(string.Format("Cannot cache item of type {0}, can only cache EntityObject",
                 item == null ? "<null>" : item.GetType().ToString()));
 
         base.AddOrReplace(key, item, expirySettings);
     }
 
     public void Configure(CacheMethodSettings cacheMethodSettings)
     {
         if (cacheMethodSettings == null) throw new ArgumentNullException("cacheMethodSettings");
         if (this.cacheMethodSettings != null)
             throw new NotSupportedException("There can be only one EF cache per method.");
 
         this.cacheMethodSettings = cacheMethodSettings;
     }
 
     protected override bool ShouldCache(object item)
     {
         return item != null;
     }
 }

If we pull the cached item out of the session cache then we don’t need to worry about attach / detach as it’s already on the correct context.  If not, we need to resolve an instance of the repository which manages the object and attach a clone of it to that (first we check if it’s not already been attached).  Now I’ve read this code again I’m not happy with the resolve, I think I need some kind of thread static AspectContext (need to be a stack I guess) which let’s me know what type /method I’m intercepting (then I could say AspectContext.CurrentCall.Service.Attach(…)).  That or somehow pass that down to the cache.  Next time I change this class I’ll refactor that I think (really appreciate suggestions if anyone has some!).

The base class is (this is the the standard cache used by the caching aspect):

public class Cache : ICache 
    {
        private readonly ICacheStore cacheStore;
        private readonly ICachedItemExpirationMonitor expirationMonitor;
        private readonly ICacheRestriction[] cacheRestrictions;
 
        public Cache(ICacheStore cacheStore, ICachedItemExpirationMonitor expirationMonitor, ICacheRestriction[] cacheRestrictions)
        {
            if (cacheStore == null) throw new ArgumentNullException("cacheStore");
            if (expirationMonitor == null) throw new ArgumentNullException("expirationMonitor");
            if (cacheRestrictions == null) throw new ArgumentNullException("cacheRestrictions");
            this.cacheStore = cacheStore;
            this.expirationMonitor = expirationMonitor;
            this.cacheRestrictions = cacheRestrictions;
        }
 
        public virtual bool TryGetValue(KeyValuePair<string, object>[] key, out object item)
        {
            CachedItem storedObject;
            if (cacheStore.TryGetValue(key, out storedObject))
            {
                if (storedObject.GetCachedItem(out item))
                {
                    return true;
                }
                cacheStore.Remove(key);
            }
            item = null;
            return false;
        }
 
        public virtual void AddOrReplace(KeyValuePair<string, object>[] key, object item)
        {
            if (!ShouldCache(item)) return;
 
            var cachedItem = new CachedItem(item);
            cacheStore.AddOrReplace(key, cachedItem);
        }
 
        public virtual void AddOrReplace(KeyValuePair<string, object>[] key, object item, CacheExpirySettings expirySettings)
        {
            if (!ShouldCache(item)) return;
 
            var cachedItem = new CachedItem(item, expirySettings);
            cacheStore.AddOrReplace(key, cachedItem);
 
            if (expirySettings.ExpiryOption != CacheExpiryOption.Never)
            {
                expirationMonitor.MonitorForExpiration(key, cacheStore, cachedItem);
            }
        }
 
        protected virtual bool ShouldCache(object item)
        {
            foreach (var restriction in cacheRestrictions)
            {
                switch (restriction.ShouldCache(item))
                {
                    case CacheRestrictionOutcome.DoNotCache:
                        return false;
                    case CacheRestrictionOutcome.ThrowException:
                        throw new CannotCacheRestrictedItemException(item);
                }
            }
            return true;
        }
    }

When you say you want caching you can give it an implementation of ICache and or ICacheStore to use (as per this tests below).

[TestMethod]
public void When_Enabling_Caching_Can_Specify_Cache_Store()
{
    var container = new WindsorContainer();
    container.Register(Component.For<ITestService>()
                           .ImplementedBy<TestServiceImpl>()
                           .EnableCaching(config =>
                                              {
                                                  config.UseCacheStore<TestCacheStore>();
                                                  config.CacheAllMethods();
                                              }));
 
    Assert.IsTrue(CachingConfiguration.ShouldCacheForService(typeof(ITestService)));
 
    var settings = CachingConfiguration.GetMethodCacheSettings(typeof (ITestService),
                                                               typeof(ITestService).GetMethod("DoSomething"), 
                                                               new Type[0]);
    Assert.IsNotNull(settings);
    Assert.AreEqual(typeof(TestCacheStore), settings.CacheTypeSettings.CacheStoreImplementation);
}
 
[TestMethod]
public void When_Enabling_Caching_Can_Specify_Cache()
{
    var container = new WindsorContainer();
    container.Register(Component.For<ITestService>()
                           .ImplementedBy<TestServiceImpl>()
                           .EnableCaching(config =>
                           {
                               config.UseCache<TestCache>();
                               config.CacheAllMethods();
                           }));
 
    Assert.IsTrue(CachingConfiguration.ShouldCacheForService(typeof(ITestService)));
 
    var settings = CachingConfiguration.GetMethodCacheSettings(typeof(ITestService),
                                                               typeof(ITestService).GetMethod("DoSomething"), 
                                                               new Type[0]);
    Assert.IsNotNull(settings);
    Assert.AreEqual(typeof(TestCache), settings.CacheTypeSettings.CacheImplementation);
}

The RepositoryComponent.Register makes sure that the right ICache is specified if you want caching enabled.

That’s pretty much it…

Advertisements

About Tom Peplow

C# .Net developer based in London and the South Coast
This entry was posted in Uncategorized and tagged , , , . Bookmark the permalink.

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