Written by 9:25 pm Programming

Implementing simple caching in MVC applications

Whenever I develop a new MVC-based website there is always a question of how best to implement caching so that it’s unobtrusive and fits neatly with the standard Repository pattern without adding too much extra coding requirement on top.

I’m going to be using dependency injection (DI) for this example so if your solution doesn’t use a DI container such as Ninject, you might need to look elsewhere — or better still consider implementing dependency injection to make your code less tightly coupled and more testable.

Cache Interface

When using DI we abstract our implementation away through the use of interfaces to de-couple the logic and allow us to easily swap out implementations as our requirements change. It also allows us to use interfaces at a lower level in our solution hierarchy and have the implementation injected at a higher level later on (a concept which is key to Onion Architecture).

Let’s start by defining a very basic cache interface called ICache:

public interface ICache
{
    T Get<T>(string key);

    void Add(string key, object value, DateTime expires);
}

For basic caching we only need two methods: one to add an item to the cache and one to retrieve it later. (You may also want a method to remove items explicitly, but I’ve omitted this for simplicity.) For the Get method, I’m using Generics so the calling code doesn’t have to deal with the explicit casting of objects retrieved from the cache.

Using HttpRuntime.Cache

In web environments, the most immediate place to turn for temporary storage is HttpRuntime.Cache. This is an in-memory, per-application-domain cache instance suitable for storing and retrieving information across user requests.

It’s worth noting that you can also access the same cache via HttpContext.Current.Cache in most scenarios, but HttpRuntime.Cache is the recommended method.

Let’s create an implementation of our ICache interface using HttpRuntime.Cache now:

public class WebCache : ICache
{  
    public T Get<T>(string key)
    {
        if (CacheNotAvailable())
            return default(T);

        return (T)Instance().Get(key);
    }

    public void Add(string key, object value, DateTime absoluteExpiration)
    {
        if (CacheNotAvailable())
            throw new InvalidOperationException("WebCache is not available in this context!");

        Instance()
            .Add(key, value, null, absoluteExpiration, Cache.NoSlidingExpiration, CacheItemPriority.Default, null);
    }

    private bool CacheNotAvailable()
    {
        return HttpRuntime.Cache == null;
    }

    private Cache Instance()
    {
        return HttpRuntime.Cache;
    }
}

From the implementation above, you can see that the first thing we do when retrieving items using the Get method is to check that the cache is available – this protects us in cases where someone may reference this class and attempt to use it outside of a web environment (such as from a Windows Forms application). In this case, we’ll just return whatever the default value is for the generic type T.

When adding objects to the cache we need to be more informative, however, so in this case, an exception will be thrown if the cache is unavailable — otherwise, the caller may think their item has been added when in fact it was not!

It’s important that we locate the implementation inside our MVC web project itself since that’s where we’ll naturally have access to HttpRuntime — if you try and add this code to a standard class library, you would need a reference to System.Web which shouldn’t really be used outside of your UI layer. I created a “Caching” folder within my MVC project and added the WebCache.cs file there.

Wiring it all Together

Now that we have both interface and a working implementation, we need to wire them up so that at run-time our DI container can fulfill requests for an ICache implementation by supplying the WebCache instance. In a standard MVC application — assuming you have added the Ninject and Ninject.MVC packages using NuGet — all DI bindings are configured within the NinjectConfig.cs class located in the App_Start folder. Open this file and add a new line in the RegisterServices method:

kernel.Bind<ICache>().To<WebCache>().InSingletonScope();

We can use Singleton scope as the cache itself is global to all requests anyway, so there is no need for the added overhead of creating new WebCache instances on a per-request or per-thread basis.

Adding Caching to a Repository

Repositories are a standard pattern for abstracting data access so that the UI (and other) layers don’t need to be concerned with how, or where, data is coming from or being persisted to. When building scalable applications I highly recommend creating a separate class library to hold your repository interfaces and implementations — this should sit between your actual data layer and the UI layer.

Also when building repository libraries I find it useful to create a BaseRepository abstract class to give all repositories some shared functionality, and this is exactly the place to hold our ICache reference:

public abstract class BaseRepository
{
    protected ICache Cache { get; set; }

    protected BaseRepository(ICache cache)
    {
        this.Cache = cache;
    }
}

When creating specific repository instances we can inherit from BaseRepository as well as implementing the repository’s interface, and the ICache instance gets passed through the constructor into the base constructor:

public class ProductRepository : BaseRepository, IProductRepository
{
    public ProductRepository(ICache cache)
        : base(cache)
    {
    }

    // Implementation of IProductRepository interface...
}

Finally, we need to make use of the cache in our repository methods:

public IList<Product> GetProducts(bool useCache = true)
{
    IList<Product> products = null;

    if (useCache)
        products = Cache.Get<IList<Product>>("products");

    if (products == null)
    {
        products = MyDbContext.Products.ToList();

        Cache.Add("products", products, DateTime.Now.AddMinutes(5));
    }

    return products;
}

The first thing to note is that we pass in an optional boolean “useCache” to give us control over whether the method will look in the cache for an existing object, or skip straight to retrieving data from the store (in this case an Entity Framework DbContext instance). This is useful in cases where we want to implement long caching for performance, but have some areas of the site (for example, an administration section) where we can see the real-time view straight from the database.

In this example, we’ve just simulated selecting the entire Products table, but we could easily have used a LINQ query to filter the results to a specific set of entities.

Using the Repository in a Controller

To tie everything together, let’s look at a sample MVC controller that might make use of the repository to display some products on a view:

public ProductController : Controller
{
    public ActionResult Index()
    {
        var productRepository = DependencyResolver.Current.GetService&lt;IProductRepository>();

        var products = productRepository.GetProducts();

        var viewModel = new ProductsViewModel();

        // Load products into view model...

        return View(viewModel);
    }
}

We use the built-in dependency resolver in MVC to get an instance of the ProductRepository via its interface IProductRepository. In doing so Ninject will also resolve the ICache interface we specified in the ProductRepository constructor and give us an instance of the WebCache class too. Then we use the GetProducts method to grab our list of products and return them to the view — I’ve skipped turning the products into a view model, but it’s good practice in MVC not to return domain entities to your view. View models are separate classes customised to contain only the properties required by your view, and help to make the code more flexible in case the needs of the view change over time but your domain model stays the same.

When we run the project, the first time our Products page is loaded, there won’t be any data in the cache and so a database call will be made and the products loaded into the cache for the specified duration (5 minutes in our earlier example). Any subsequent requests to this page will get the cached version, speeding up page load considerably and reducing the overhead on the database, albeit at the expense of some visitors viewing data that may be up to 5 minutes out of date.

When deciding how long to cache items for, I generally try and consider the following when setting expiration times:

  • How often will the data change? If you know the records are only updated on an hourly basis, you can set expiration to reflect this.
  • How expensive is it to load the data? Complex queries that execute slowly are better suited to caching than simple queries the database can process in milliseconds.
  • How often is the data used? Information on the home page of your website is likely to be seen significantly more often than data located elsewhere. If you have a high traffic site, caching as much of the homepage as possible will really speed up loading times, improving your visitor experience and in many cases boosting your search rankings too!
  • How big is the data? Your website has a finite amount of memory, and caching uses server RAM to store information for fast retrieval. Filling up the cache with huge amounts of information risks causing low memory issues on your server that could impact the entire website or other services running on the machine. Only cache data that you need to load quickly, and set realistic expiration times to reduce memory overhead.

Scaling Up

As your website grows, there may come a time where the HttpRuntime.Cache isn’t sufficient anymore – for example, if you need to spread the load over multiple web servers, it’s no good having a cache that exists on each box separately, meaning all items have to be stored twice.

By abstracting our implementation behind the ICache interface, it would be trivial to replace the WebCache with a more enterprise-level caching solution (such as AppFabric) without having to change any of your application code. Simply write a new implementation of ICache that leverages AppFabric caching internally, and change the NinjectConfig.cs to reference the new cache implementation:

kernel.Bind&lt;ICache>().To&lt;AppFabricCache>().InSingletonScope();

Summary

This example shows how a basic but effective caching scheme can be implemented into any MVC web applications, using dependency injection to decouple the implementation and allowing us to swap out cache implementations without re-writing significant chunks of your application code.

(Visited 277 times, 1 visits today)
Tags: , , , Last modified: 16/02/2020
Close