Friday, March 1, 2013

Caching System: Pass One

This post is based on my previous post about caching versus request reduction.

Okay, building a self-managed caching system on top of MVC (3+) and Razor. Frankly, this makes me miss WebForms since some things are harder in Razor.

So the workflow is as follows: On initial request the server checks a cookie to see whether to include a full section or whether to include a stub.

@if (ClientCache.OnClient("trial"))
{
<clientcache cacheid="trial"></clientcache>
} else
{
    <cacheable cacheid="trial">
    <div >
    … content …
    </div>
    </cacheable>
}

A couple things of note. I am using custom HTML tags. In this case I think it is a technique that makes sense. I am using a client cache variable on the WebViewPage. I created this by descending from WebViewPage. Here is the very boring class:

public abstract class ClientCachingWebViewPage<TModel> : WebViewPage<TModel>
{
    private ClientCache _clientCache;
    public ClientCache ClientCache
    {
        get { return _clientCache ?? (_clientCache = new ClientCache()); }
    }
}

*yawn* So the server determines whether the client has cached the section by checking the cookie. Here is the object that does the checking:

public class ClientCache
{
    private string[] _cacheList;
    
    public const string CacheCookieName = "ClientCache";
    public const char Divider = ':';

    public string[] ClientCacheIds
    {
        get
        {
            if (_cacheList == null)
            {
                HttpCookie cookie = HttpContext.Current.Request.Cookies[CacheCookieName];
                _cacheList = cookie != null
                    ? cookie.Value.Split(new[] { Divider })
                    : new string[0];
            }
            return _cacheList;
        }
    }

    public bool OnClient(string id)
    {
        return ClientCacheIds.Contains(id);
    }
}

Okay, so that all there is on the server. A check on the cookie and a conditional that includes a stub. On the client the code is as follows:

// get all cacheable sections and store in web storage
$("cacheable").each(function () {
    var key = $(this).attr("cacheId");
    var value = $(this).html();
    localStorage.setItem(key, value);
    var myCookie = $.cookie("ClientCache");
    myCookie = myCookie ? myCookie + ":" + key : key;
    $.cookie("clientCache", myCookie, { expires: 365 });
});
// update all stubs with cached values
$("clientcache").each(function () {
    var key = $(this).attr("cacheId");
    var value = localStorage.getItem(key);
    $(this).html(value);
});

Note that I am using Jquery and a Jquery cookie plugin. In a real implementation you might not want to do this because then you can't cache these libraries with this technique.

So this works pretty well for a first attempt. It also seems like you could easily build a system that loaded cacheable sections for later pages after a page has loaded. I will maybe do that later. But there are a couple issues. One is that you can't cache a volatile section, well you can, but it will not work correctly. This is built with the notion that your markup or templates are going to be separate from your volatile data.

The other issue that really bothers me is the markup in the cshtml file. I hate how it looks. I would much rather something like:

@ClientCache("cacheid")
{
...content…
}

So doing this with WebForms would be easy, but with Razor it isn't so clear. You could do something like RenderSection or Html.Partial but this would move the markup out of the page, but maybe that isn't so onerous. Another is to use Razor templates although I worry that would cause references in the Razor to work badly.

This whole system also falls to pieces if you clear local storage without updating the cookie so you need a system that scans that scans the web storage for the ids it thinks it has and if needed does another request to get the sections that are missing. This can tie in to a mechanism that does the predictive caching.

No comments:

Post a Comment