Using the Caching Infrastructure in ASP.NET

  • Tutorial
A year and a half ago, I wrote an article about caching in ASP.NET MVC , in which I described how to improve the performance of ASP.NET MVC applications by caching both on the server and on the client. In the comments to the article, many additional ways were mentioned for managing caching in ASP.NET.

In that post, I’ll show you how to use the capabilities of the ASP.NET infrastructure to manage caching.


HTTP caching (revisited)


In the last post, there was a monstrous code example for implementing HTTP caching when returning the basket state:

Code example
[HttpGet]
public ActionResult CartSummary()
{
    //Кеширование только на клиенте, обновление при каждом запросе
    this.Response.Cache.SetCacheability(System.Web.HttpCacheability.Private);
    this.Response.Cache.SetMaxAge(TimeSpan.Zero);
    var cart = ShoppingCart.GetCart(this.HttpContext);
    var cacheKey = "shooting-cart-" + cart.ShoppingCartId;
    var cachedPair = (Tuple)this.HttpContext.Cache[cacheKey];
    if (cachedPair != null) //Если данные есть в кеше на сервере
    {
        //Устанавливаем Last-Modified
        this.Response.Cache.SetLastModified(cachedPair.Item1);
        var lastModified = DateTime.MinValue;
        //Обрабатываем Conditional Get
        if (DateTime.TryParse(this.Request.Headers["If-Modified-Since"], out lastModified)
                && lastModified >= cachedPair.Item1)
        {
            return new NotModifiedResult();
        }
        ViewData["CartCount"] = cachedPair.Item2;
    }
    else //Если данных нет в кеше на сервере
    {
        //Текущее время, округленное до секунды
        var now = DateTime.Now;
        now = new DateTime(now.Year, now.Month, now.Day,
                            now.Hour, now.Minute, now.Second);
        //Устанавливаем Last-Modified
        this.Response.Cache.SetLastModified(now);
        var count = cart.GetCount();
        this.HttpContext.Cache[cacheKey] = Tuple.Create(now, count);
        ViewData["CartCount"] = count;
    }
    return PartialView("CartSummary");
}


for comparison - the original version (without caching)
        public ActionResult CartSummary()
        {
            var cart = ShoppingCart.GetCart(this.HttpContext);
            ViewData["CartCount"] = cart.GetCount();
            return PartialView("CartSummary");
        }


If this is the first time you see this code and don’t know where it came from, then read the previous article .


Naturally, for each case of caching, writing such code is very inconvenient. The ASP.NET infrastructure already has a ready-made infrastructure that allows you to achieve the same result with much less code.

Cache dependencies


In ASP.NET, you can bind server responses to items in the cache ( System.Web.Caching.Cache).

This is done with one function:
Response.AddCacheItemDependency(cacheKey);

But the binding itself does not give anything. In order to handle Conditional-GET, you must give the header Last-Modifiedand / or E-Tag. There are also functions for this:
Response.Cache.SetLastModifiedFromFileDependencies();
Response.Cache.SetETagFromFileDependencies();

Despite the word Filein the function name, any dependencies of the answer are analyzed. Moreover, if the response to the server has many dependencies, then it is Last-Modifiedset to the highest value, and E-Tagis formed from all the dependencies.

The next step is to enable response caching on the server and on the client, because ASP.NET can only process Conditional-GET for responses cached on the server:
Response.Cache.SetCacheability(HttpCacheability.ServerAndPrivate);

In carrying out these four lines of code ASP.NET renders headers Last-Modified, E-Tag, Cache-Control: privateand stores the response to the server. But there is a problem - IE does not request a new version of the page, caching the default response for a day or until the browser restarts. In general, caching a response without specifying max-age or an Expires header can vary greatly between browsers.

To defeat this problem you must specify max-age=0. In ASP.NET, this can be done with the following function:
Response.Cache.SetMaxAge(TimeSpan.FromSeconds(0));

But this function also sets the response cache lifetime on the server, and, in fact, ASP.NET stops giving cached server responses.

The correct way to achieve the result:
Response.Cache.AppendCacheExtension("max-age=0")

Then the response is cached on the server, but the header is given to the client Cache-Control: private, max-age=0, which forces the browser to send a request each time. Unfortunately, this method is not documented anywhere.

As a result, ASP.NET processes Conditional-GET and returns responses from the server cache while the element with the key is stored in the ASP.NET cache and not changed cacheKey.

Full action code:
[HttpGet]
public ActionResult CartSummary()
{
    var cart = ShoppingCart.GetCart(this.HttpContext);
    var cacheKey = "shopping-cart-" + cart.ShoppingCartId;
    ViewData["CartCount"] = GetCachedCount(cart, cacheKey);
    this.Response.AddCacheItemDependency(cacheKey);
    this.Response.Cache.SetLastModifiedFromFileDependencies();
    this.Response.Cache.AppendCacheExtension("max-age=0");
    this.Response.Cache.SetCacheability(HttpCacheability.ServerAndPrivate);
    return PartialView("CartSummary");
}
private int GetCachedCount(ShoppingCart cart,string cacheKey)
{
    var value = this.HttpContext.Cache[cacheKey];
    int result = 0;
    if (value != null)
    {
        result = (int) value;
    }
    else
    {
        result = cart.GetCount();
        this.HttpContext.Cache.Insert(cacheKey,result);
    }
    return result;
}

Agree, this is much less code than in the previous article.

Cache variation


By default, ASP.NET caches one response for any user by one url (excluding querystring). This leads to the fact that in the example above, the same answer will be given to all users.

By the way, the behavior of ASP.NET is different from that laid down in the HTTP protocol, which caches the response by the full url. The HTTP protocol provides the ability to vary the cache using the Vary response header. In ASP.NET, you can also vary the response by parameters in QueryString, by encoding (header Accept-Encoding), as well as by custom parameter attached to the response.

Variation by custom parameter allows you to save the cache for different users. In order to give different baskets to different users, you need:

1) Add a call to the controller
Response.Cache.SetVaryByCustom("sessionId");


2) In Global.asaxoverride methodGetVaryByCustomString
public override string GetVaryByCustomString(HttpContext context, string custom)
{
    if (custom == "sessionId")
    {
        var sessionCookie = context.Request.Cookies["ASP.NET_SessionId"];
        if (sessionCookie != null)
        {
            return sessionCookie.Value;
        }
    }
    return base.GetVaryByCustomString(context, custom);
}

Thus, different cache instances will be given for different sessions.

With such an implementation, it must be remembered that each server response is stored in the cache on the server. If you save large pages for each user, they will often be squeezed out of the cache and this will lead to a decrease in caching efficiency.

Dependencies between cache elements


The dependency mechanism in ASP.NET allows you to bind not only the response to an element of the internal cache, but also bind one element of the cache to another. The class CacheDependencyand its heirs are responsible for this .

For instance:
HttpContext.Cache.Insert("cacheItemKey",data, new CacheDependency(null, new[] { "anotherCacheItemKey" }));


If an element with a key anotherCacheItemKeyis changed or removed from the cache, then an element with a key cacheItemKey will be automatically deleted from the cache.

This allows you to build systems with a multi-level synchronized cache.

Additional features


The cache dependency mechanism in ASP.NET is extensible. By default, you can create dependencies on elements of the internal cache, dependencies on files and folders, as well as dependencies on tables in the database. In addition, you can create your own cache dependency classes, for example for Redis.

But more about that in the following articles.

Also popular now: