c# asp.net - How to implement a caching model without violating MVC pattern?




structure database (5)

I have an ASP.NET MVC 3 (Razor) Web Application, with a particular page which is highly database intensive, and user experience is of the upmost priority.

Thus, i am introducing caching on this particular page.

I'm trying to figure out a way to implement this caching pattern whilst keeping my controller thin, like it currently is without caching:

public PartialViewResult GetLocationStuff(SearchPreferences searchPreferences)
{
   var results = _locationService.FindStuffByCriteria(searchPreferences);
   return PartialView("SearchResults", results);
}

As you can see, the controller is very thin, as it should be. It doesn't care about how/where it is getting it's info from - that is the job of the service.

A couple of notes on the flow of control:

  1. Controllers get DI'ed a particular Service, depending on it's area. In this example, this controller get's a LocationService
  2. Services call through to an IQueryable<T> Repository and materialize results into T or ICollection<T>.

How i want to implement caching:

  • I can't use Output Caching - for a few reasons. First of all, this action method is invoked from the client-side (jQuery/AJAX), via [HttpPost], which according to HTTP standards should not be cached as a request. Secondly, i don't want to cache purely based on the HTTP request arguments - the cache logic is a lot more complicated than that - there is actually two-level caching going on.
  • As i hint to above, i need to use regular data-caching, e.g Cache["somekey"] = someObj;.
  • I don't want to implement a generic caching mechanism where all calls via the service go through the cache first - i only want caching on this particular action method.

First thought's would tell me to create another service (which inherits LocationService), and provide the caching workflow there (check cache first, if not there call db, add to cache, return result).

That has two problems:

  1. The services are basic Class Libraries - no references to anything extra. I would need to add a reference to System.Web here.
  2. I would have to access the HTTP Context outside of the web application, which is considered bad practice, not only for testability, but in general - right?

I also thought about using the Models folder in the Web Application (which i currently use only for ViewModels), but having a cache service in a models folder just doesn't sound right.

So - any ideas? Is there a MVC-specific thing (like Action Filter's, for example) i can use here?

General advice/tips would be greatly appreciated.


Answers

An action attribute seems like a good way to achieve this. Here's an example (disclaimer: I am writing this from the top of my head: I've consumed a certain quantity of beer when writing this so make sure you test it extensively :-)):

public class CacheModelAttribute : ActionFilterAttribute
{
    private readonly string[] _paramNames;
    public CacheModelAttribute(params string[] paramNames)
    {
        // The request parameter names that will be used 
        // to constitute the cache key.
        _paramNames = paramNames;
    }

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        base.OnActionExecuting(filterContext);
        var cache = filterContext.HttpContext.Cache;
        var model = cache[GetCacheKey(filterContext.HttpContext)];
        if (model != null)
        {
            // If the cache contains a model, fetch this model
            // from the cache and short-circuit the execution of the action
            // to avoid hitting the repository
            var result = new ViewResult
            {
                ViewData = new ViewDataDictionary(model)
            };
            filterContext.Result = result;
        }
    }

    public override void OnResultExecuted(ResultExecutedContext filterContext)
    {
        base.OnResultExecuted(filterContext);
        var result = filterContext.Result as ViewResultBase;
        var cacheKey = GetCacheKey(filterContext.HttpContext);
        var cache = filterContext.HttpContext.Cache;
        if (result != null && result.Model != null && cache[key] == null)
        {
            // If the action returned some model, 
            // store this model into the cache
            cache[key] = result.Model;
        }
    }

    private string GetCacheKey(HttpContextBase context)
    {
        // Use the request values of the parameter names passed
        // in the attribute to calculate the cache key.
        // This function could be adapted based on the requirements.
        return string.Join(
            "_", 
            (_paramNames ?? Enumerable.Empty<string>())
                .Select(pn => (context.Request[pn] ?? string.Empty).ToString())
                .ToArray()
        );
    }
}

And then your controller action could look like this:

[CacheModel("id", "name")]
public PartialViewResult GetLocationStuff(SearchPreferences searchPreferences)
{
   var results = _locationService.FindStuffByCriteria(searchPreferences);
   return View(results);
}

And as far as your problem with referencing the System.Web assembly in the service layer is concerned, that's no longer a problem in .NET 4.0. There's a completely new assembly which provides extensible caching features : System.Runtime.Caching, so you could use this to implement caching in your service layer directly.

Or even better if you are using an ORM at your service layer probably this ORM provides caching capabilities? I hope it does. For example NHibernate provides a second level cache.


I've accepted @Josh's answer, but thought i'd add my own answer, because i didn't exactly go with what he suggested (close), so thought for completeness i'd add what i actually did.

The key is i am now using System.Runtime.Caching. Because this exists in an assembly which is .NET specific and not ASP.NET specific, i have no problems referencing this in my service.

So all i've done is put the caching logic in the specific service layer methods that need the caching.

And an important point, im working off System.Runtime.Caching.ObjectCache class - this is what get's injected into the constructor of the service.

My current DI injects a System.Runtime.Caching.MemoryCache object. The good thing about the ObjectCache class is that it is abstract and all the core methods are virtual.

Which means for my unit tests, i have created a MockCache class, overriding all methods and implementing the underlying cache mechanism with a simple Dictionary<TKey,TValue>.

We plan to switch over to Velocity soon - so again, all i need to do is create another ObjectCache deriving class and i'm good to go.

Thanks for the help everyone!


It sounds like you're trying to cache the data you are getting from your database. Here's how I handle this (an approach that I've seen used in many open-source MVC projects):

    /// <summary>
    /// remove a cached object from the HttpRuntime.Cache
    /// </summary>
    public static void RemoveCachedObject(string key)
    {
        HttpRuntime.Cache.Remove(key);
    }

    /// <summary>
    /// retrieve an object from the HttpRuntime.Cache
    /// </summary>
    public static object GetCachedObject(string key)
    {
        return HttpRuntime.Cache[key];
    }

    /// <summary>
    /// add an object to the HttpRuntime.Cache with an absolute expiration time
    /// </summary>
    public static void SetCachedObject(string key, object o, int durationSecs)
    {
        HttpRuntime.Cache.Add(
            key,
            o,
            null,
            DateTime.Now.AddSeconds(durationSecs),
            Cache.NoSlidingExpiration,
            CacheItemPriority.High,
            null);
    }

    /// <summary>
    /// add an object to the HttpRuntime.Cache with a sliding expiration time. sliding means the expiration timer is reset each time the object is accessed, so it expires 20 minutes, for example, after it is last accessed.
    /// </summary>
    public static void SetCachedObjectSliding(string key, object o, int slidingSecs)
    {
        HttpRuntime.Cache.Add(
            key,
            o,
            null,
            Cache.NoAbsoluteExpiration,
            new TimeSpan(0, 0, slidingSecs),
            CacheItemPriority.High,
            null);
    }

    /// <summary>
    /// add a non-removable, non-expiring object to the HttpRuntime.Cache
    /// </summary>
    public static void SetCachedObjectPermanent(string key, object o)
    {
        HttpRuntime.Cache.Remove(key);
        HttpRuntime.Cache.Add(
            key,
            o,
            null,
            Cache.NoAbsoluteExpiration,
            Cache.NoSlidingExpiration,
            CacheItemPriority.NotRemovable,
            null);
    }

I have those methods in a static class named Current.cs. Here's how you can apply those methods to your controller action:

public PartialViewResult GetLocationStuff(SearchPreferences searchPreferences)
{
   var prefs = (object)searchPreferences;
   var cachedObject = Current.GetCachedObject(prefs); // check cache
   if(cachedObject != null) return PartialView("SearchResults", cachedObject);

   var results = _locationService.FindStuffByCriteria(searchPreferences);
   Current.SetCachedObject(prefs, results, 60); // add to cache for 60 seconds

   return PartialView("SearchResults", results);
}

My answer is based on the assumption that your services implement an interface, for example the type of _locationService is actually ILocationService but is injected with a concrete LocationService. Create a CachingLocationService that implements the ILocationService interface and change your container configuration to inject that caching version of the service to this controller. The CachingLocationService would itself have a dependecy on ILocationService which would be injected with the original LocationService class. It would use this to execute the real business logic and concern itself only with pulling and pushing from cache.

You don't need to create CachingLocationService in the same assembly as the original LocationService. It could be in your web assembly. However, personally I'd put it in the original assembly and add the new reference.

As for adding a dependency on HttpContext; you can remove this by taking a dependency on

Func<HttpContextBase> 

and injecting this at runtime with something like

() => HttpContext.Current

Then in your tests you can mock HttpContextBase, but you may have trouble mocking the Cache object without using something like TypeMock.


Edit: On further reading up on the .NET 4 System.Runtime.Caching namespace, your CachingLocationService should take a dependency on ObjectCache. This is the abstract base class for cache implementations. You could then inject that with System.Runtime.Caching.MemoryCache.Default, for instance.








c# asp.net-mvc caching architecture asp.net-mvc-3