Loosely coupled .NET Cache Provider using Dependency Injection

Posted by Rhames on Geeks with Blogs See other posts from Geeks with Blogs or by Rhames
Published on Tue, 11 Sep 2012 12:11:10 GMT Indexed on 2012/09/11 15:39 UTC
Read the original article Hit count: 247

Filed under:

I have recently been reading the excellent book “Dependency Injection in .NET”, written by Mark Seemann. I do not generally buy software development related books, as I never seem to have the time to read them, but I have found the time to read Mark’s book, and it was time well spent I think.

Reading the ideas around Dependency Injection made me realise that the Cache Provider code I wrote about earlier (see http://geekswithblogs.net/Rhames/archive/2011/01/10/using-the-asp.net-cache-to-cache-data-in-a-model.aspx) could be refactored to use Dependency Injection, which should produce cleaner code.

The goals are to:

  • Separate the cache provider implementation (using the ASP.NET data cache) from the consumers (loose coupling). This will also mean that the dependency on System.Web for the cache provider does not ripple down into the layers where it is being consumed (such as the domain layer).
  • Provide a decorator pattern to allow a consumer of the cache provider to be implemented separately from the base consumer (i.e. if we have a base repository, we can decorate this with a caching version). Although I used the term repository, in reality the cache consumer could be just about anything.
  • Use constructor injection to provide the Dependency Injection, with a suitable DI container (I use Castle Windsor).

The sample code for this post is available on github, https://github.com/RobinHames/CacheProvider.git

ICacheProvider

In the sample code, the key interface is ICacheProvider, which is in the domain layer.

   1:  using System;
   2:  using System.Collections.Generic;
   3:   
   4:  namespace CacheDiSample.Domain
   5:  {
   6:      public interface ICacheProvider<T>
   7:      {
   8:          T Fetch(string key, Func<T> retrieveData, DateTime? absoluteExpiry, TimeSpan? relativeExpiry);
   9:          IEnumerable<T> Fetch(string key, Func<IEnumerable<T>> retrieveData, DateTime? absoluteExpiry, TimeSpan? relativeExpiry);
  10:      }
  11:  }

 

This interface contains two methods to retrieve data from the cache, either as a single instance or as an IEnumerable. the second paramerter is of type Func<T>. This is the method used to retrieve data if nothing is found in the cache.

The ASP.NET implementation of the ICacheProvider interface needs to live in a project that has a reference to system.web, typically this will be the root UI project, or it could be a separate project. The key thing is that the domain or data access layers do not need system.web references adding to them.

In my sample MVC application, the CacheProvider is implemented in the UI project, in a folder called “CacheProviders”:

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq;
   4:  using System.Web;
   5:  using System.Web.Caching;
   6:  using CacheDiSample.Domain;
   7:   
   8:  namespace CacheDiSample.CacheProvider
   9:  {
  10:      public class CacheProvider<T> : ICacheProvider<T>
  11:      {
  12:          public T Fetch(string key, Func<T> retrieveData, DateTime? absoluteExpiry, TimeSpan? relativeExpiry)
  13:          {
  14:              return FetchAndCache<T>(key, retrieveData, absoluteExpiry, relativeExpiry);
  15:          }
  16:   
  17:          public IEnumerable<T> Fetch(string key, Func<IEnumerable<T>> retrieveData, DateTime? absoluteExpiry, TimeSpan? relativeExpiry)
  18:          {
  19:              return FetchAndCache<IEnumerable<T>>(key, retrieveData, absoluteExpiry, relativeExpiry);
  20:          }
  21:   
  22:          #region Helper Methods
  23:   
  24:          private U FetchAndCache<U>(string key, Func<U> retrieveData, DateTime? absoluteExpiry, TimeSpan? relativeExpiry)
  25:          {
  26:              U value;
  27:              if (!TryGetValue<U>(key, out value))
  28:              {
  29:                  value = retrieveData();
  30:                  if (!absoluteExpiry.HasValue)
  31:                      absoluteExpiry = Cache.NoAbsoluteExpiration;
  32:   
  33:                  if (!relativeExpiry.HasValue)
  34:                      relativeExpiry = Cache.NoSlidingExpiration;
  35:   
  36:                  HttpContext.Current.Cache.Insert(key, value, null, absoluteExpiry.Value, relativeExpiry.Value);
  37:              }
  38:              return value;
  39:          }
  40:   
  41:          private bool TryGetValue<U>(string key, out U value)
  42:          {
  43:              object cachedValue = HttpContext.Current.Cache.Get(key);
  44:              if (cachedValue == null)
  45:              {
  46:                  value = default(U);
  47:                  return false;
  48:              }
  49:              else
  50:              {
  51:                  try
  52:                  {
  53:                      value = (U)cachedValue;
  54:                      return true;
  55:                  }
  56:                  catch
  57:                  {
  58:                      value = default(U);
  59:                      return false;
  60:                  }
  61:              }
  62:          }
  63:   
  64:          #endregion
  65:   
  66:      }
  67:  }

 

The FetchAndCache helper method checks if the specified cache key exists, if it does not, the Func<U> retrieveData method is called, and the results are added to the cache.

Using Castle Windsor to register the cache provider

In the MVC UI project (my application root), Castle Windsor is used to register the CacheProvider implementation, using a Windsor Installer:

   1:  using Castle.MicroKernel.Registration;
   2:  using Castle.MicroKernel.SubSystems.Configuration;
   3:  using Castle.Windsor;
   4:   
   5:  using CacheDiSample.Domain;
   6:  using CacheDiSample.CacheProvider;
   7:   
   8:  namespace CacheDiSample.WindsorInstallers
   9:  {
  10:      public class CacheInstaller : IWindsorInstaller
  11:      {
  12:          public void Install(IWindsorContainer container, IConfigurationStore store)
  13:          {
  14:              container.Register(
  15:                  Component.For(typeof(ICacheProvider<>))
  16:                  .ImplementedBy(typeof(CacheProvider<>))
  17:                  .LifestyleTransient());
  18:          }
  19:      }
  20:  }

 

Note that the cache provider is registered as a open generic type.

Consuming a Repository

I have an existing couple of repository interfaces defined in my domain layer:

IRepository.cs

   1:  using System;
   2:  using System.Collections.Generic;
   3:   
   4:  using CacheDiSample.Domain.Model;
   5:   
   6:  namespace CacheDiSample.Domain.Repositories
   7:  {
   8:      public interface IRepository<T>
   9:          where T : EntityBase
  10:      {
  11:          T GetById(int id);
  12:          IList<T> GetAll();
  13:      }
  14:  }

 

IBlogRepository.cs

   1:  using System;
   2:  using CacheDiSample.Domain.Model;
   3:   
   4:  namespace CacheDiSample.Domain.Repositories
   5:  {
   6:      public interface IBlogRepository : IRepository<Blog>
   7:      {
   8:          Blog GetByName(string name);
   9:      }
  10:  }

 

These two repositories are implemented in the DataAccess layer, using Entity Framework to retrieve data (this is not important though). One important point is that in the BaseRepository implementation of IRepository, the methods are virtual. This will allow the decorator to override them.

The BlogRepository is registered in a RepositoriesInstaller, again in the MVC UI project.

   1:  using Castle.MicroKernel.Registration;
   2:  using Castle.MicroKernel.SubSystems.Configuration;
   3:  using Castle.Windsor;
   4:   
   5:  using CacheDiSample.Domain.CacheDecorators;
   6:  using CacheDiSample.Domain.Repositories;
   7:  using CacheDiSample.DataAccess;
   8:   
   9:  namespace CacheDiSample.WindsorInstallers
  10:  {
  11:      public class RepositoriesInstaller : IWindsorInstaller
  12:      {
  13:          public void Install(IWindsorContainer container, IConfigurationStore store)
  14:          {
  15:              container.Register(Component.For<IBlogRepository>()
  16:                  .ImplementedBy<BlogRepository>()
  17:                  .LifestyleTransient()
  18:                  .DependsOn(new 
  19:                                  {
  20:                                      nameOrConnectionString = "BloggingContext"
  21:                                  }));
  22:          }
  23:      }
  24:  }

 

Now I can inject a dependency on the IBlogRepository into a consumer, such as a controller in my sample code:

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq;
   4:  using System.Web;
   5:  using System.Web.Mvc;
   6:   
   7:  using CacheDiSample.Domain.Repositories;
   8:  using CacheDiSample.Domain.Model;
   9:   
  10:  namespace CacheDiSample.Controllers
  11:  {
  12:      public class HomeController : Controller
  13:      {
  14:          private readonly IBlogRepository blogRepository;
  15:   
  16:          public HomeController(IBlogRepository blogRepository)
  17:          {
  18:              if (blogRepository == null)
  19:                  throw new ArgumentNullException("blogRepository");
  20:   
  21:              this.blogRepository = blogRepository;
  22:          }
  23:   
  24:          public ActionResult Index()
  25:          {
  26:              ViewBag.Message = "Welcome to ASP.NET MVC!";
  27:   
  28:              var blogs = blogRepository.GetAll();
  29:   
  30:              return View(new Models.HomeModel { Blogs = blogs });
  31:          }
  32:   
  33:          public ActionResult About()
  34:          {
  35:              return View();
  36:          }
  37:      }
  38:  }

 

Consuming the Cache Provider via a Decorator

I used a Decorator pattern to consume the cache provider, this means my repositories follow the open/closed principle, as they do not require any modifications to implement the caching. It also means that my controllers do not have any knowledge of the caching taking place, as the DI container will simply inject the decorator instead of the root implementation of the repository.

The first step is to implement a BlogRepository decorator, with the caching logic in it. Note that this can reside in the domain layer, as it does not require any knowledge of the data access methods.

BlogRepositoryWithCaching.cs

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq;
   4:  using System.Text;
   5:   
   6:  using CacheDiSample.Domain.Model;
   7:  using CacheDiSample.Domain;
   8:  using CacheDiSample.Domain.Repositories;
   9:   
  10:  namespace CacheDiSample.Domain.CacheDecorators
  11:  {
  12:      public class BlogRepositoryWithCaching : IBlogRepository
  13:      {
  14:          // The generic cache provider, injected by DI
  15:          private ICacheProvider<Blog> cacheProvider;
  16:          // The decorated blog repository, injected by DI
  17:          private IBlogRepository parentBlogRepository;
  18:   
  19:          public BlogRepositoryWithCaching(IBlogRepository parentBlogRepository, ICacheProvider<Blog> cacheProvider)
  20:          {
  21:              if (parentBlogRepository == null)
  22:                  throw new ArgumentNullException("parentBlogRepository");
  23:   
  24:              this.parentBlogRepository = parentBlogRepository;
  25:   
  26:              if (cacheProvider == null)
  27:                  throw new ArgumentNullException("cacheProvider");
  28:   
  29:              this.cacheProvider = cacheProvider;
  30:          }
  31:   
  32:          public Blog GetByName(string name)
  33:          {
  34:              string key = string.Format("CacheDiSample.DataAccess.GetByName.{0}", name);
  35:              // hard code 5 minute expiry!
  36:              TimeSpan relativeCacheExpiry = new TimeSpan(0, 5, 0);
  37:              return cacheProvider.Fetch(key, () =>
  38:              {
  39:                  return parentBlogRepository.GetByName(name);
  40:              },
  41:                  null, relativeCacheExpiry);
  42:          }
  43:   
  44:          public Blog GetById(int id)
  45:          {
  46:              string key = string.Format("CacheDiSample.DataAccess.GetById.{0}", id);
  47:   
  48:              // hard code 5 minute expiry!
  49:              TimeSpan relativeCacheExpiry = new TimeSpan(0, 5, 0);
  50:              return cacheProvider.Fetch(key, () =>
  51:              {
  52:                  return parentBlogRepository.GetById(id);
  53:              },
  54:                  null, relativeCacheExpiry);
  55:          }
  56:   
  57:          public IList<Blog> GetAll()
  58:          {
  59:              string key = string.Format("CacheDiSample.DataAccess.GetAll");
  60:   
  61:              // hard code 5 minute expiry!
  62:              TimeSpan relativeCacheExpiry = new TimeSpan(0, 5, 0);
  63:              return cacheProvider.Fetch(key, () =>
  64:              {
  65:                  return parentBlogRepository.GetAll();
  66:              },
  67:                  null, relativeCacheExpiry)
  68:              .ToList();
  69:          }
  70:      }
  71:  }

 

The key things in this caching repository are:

  1. I inject into the repository the ICacheProvider<Blog> implementation, via the constructor. This will make the cache provider functionality available to the repository.
  2. I inject the parent IBlogRepository implementation (which has the actual data access code), via the constructor. This will allow the methods implemented in the parent to be called if nothing is found in the cache.
  3. I override each of the methods implemented in the repository, including those implemented in the generic BaseRepository. Each override of these methods follows the same pattern. It makes a call to the CacheProvider.Fetch method, and passes in the parentBlogRepository implementation of the method as the retrieval method, to be used if nothing is present in the cache.

Configuring the Caching Repository in the DI Container

The final piece of the jigsaw is to tell Castle Windsor to use the BlogRepositoryWithCaching implementation of IBlogRepository, but to inject the actual Data Access implementation into this decorator. This is easily achieved by modifying the RepositoriesInstaller to use Windsor’s implicit decorator wiring:

   1:  using Castle.MicroKernel.Registration;
   2:  using Castle.MicroKernel.SubSystems.Configuration;
   3:  using Castle.Windsor;
   4:   
   5:  using CacheDiSample.Domain.CacheDecorators;
   6:  using CacheDiSample.Domain.Repositories;
   7:  using CacheDiSample.DataAccess;
   8:   
   9:  namespace CacheDiSample.WindsorInstallers
  10:  {
  11:      public class RepositoriesInstaller : IWindsorInstaller
  12:      {
  13:          public void Install(IWindsorContainer container, IConfigurationStore store)
  14:          {
  15:   
  16:              // Use Castle Windsor implicit wiring for the block repository decorator
  17:              // Register the outermost decorator first
  18:              container.Register(Component.For<IBlogRepository>()
  19:                  .ImplementedBy<BlogRepositoryWithCaching>()
  20:                  .LifestyleTransient());
  21:              // Next register the IBlogRepository inmplementation to inject into the outer decorator
  22:              container.Register(Component.For<IBlogRepository>()
  23:                  .ImplementedBy<BlogRepository>()
  24:                  .LifestyleTransient()
  25:                  .DependsOn(new 
  26:                                  {
  27:                                      nameOrConnectionString = "BloggingContext"
  28:                                  }));
  29:          }
  30:      }
  31:  }

 

This is all that is needed. Now if the consumer of the repository makes a call to the repositories method, it will be routed via the caching mechanism. You can test this by stepping through the code, and seeing that the DataAccess.BlogRepository code is only called if there is no data in the cache, or this has expired.

The next step is to add the SQL Cache Dependency support into this pattern, this will be a future post.

© Geeks with Blogs or respective owner