📜 ⬆️ ⬇️

Caching in ASP.NET MVC

In the last post, I talked about different caching strategies. There was a naked theory, which is already known to everyone, and to whom it is not known, that without examples nothing is clear.

In this post I want to show an example of caching in an ASP.NET MVC application and what architectural changes will have to be made to support caching.


')
For example, I took the MVC Music Store application, which is used in the training section on the asp.net website . The application is an online store, with a basket, a catalog of products and a small admin.

Investigating the problem


Immediately created a load test for one minute, which opens the main page. It turned out 60 pages per second (all tests run in debug). This is very little, it is useful to understand what the problem is.

Homepage controller code:
public ActionResult Index() { // Get most popular albums var albums = GetTopSellingAlbums(5); return View(albums); } private List<Album> GetTopSellingAlbums(int count) { // Group the order details by album and return // the albums with the highest count return storeDB.Albums .OrderByDescending(a => a.OrderDetails.Count()) .Take(count) .ToList(); } 


The main page displays an aggregate query, which will have to read most of the base to display the result, and the changes on the main page occur infrequently.

At the same time, on each page personalized information is displayed - the number of items in the cart.
Code _layout.cshtml (Razor):
 <div id="header"> <h1><a href="/">ASP.NET MVC MUSIC STORE</a></h1> <ul id="navlist"> <li class="first"><a href="@Url.Content("~")" id="current">Home</a></li> <li><a href="@Url.Content("~/Store/")">Store</a></li> <li>@{Html.RenderAction("CartSummary", "ShoppingCart");}</li> <li><a href="@Url.Content("~/StoreManager/")">Admin</a></li> </ul> </div> 


Such a “pattern” is often found in web applications. On the main page, which opens most often, statistical information is displayed in one place, which requires a lot of computational cost and changes infrequently, and in another place personalized information, which often changes. Because of this, the home page is slow and cannot be cached using HTTP.

Making the application suitable for caching.


In order for this situation, as described above, to not occur, it is necessary to separate the requests and assemble the parts of the page on the client. In ASP.NET MVC, this is pretty easy to do.
Code _layout.cshtml (Razor):
 <div id="header"> <h1><a href="/">ASP.NET MVC MUSIC STORE</a></h1> <ul id="navlist"> <li class="first"><a href="@Url.Content("~")" id="current">Home</a></li> <li><a href="@Url.Content("~/Store/")">Store</a></li> <li><span id="shopping-cart"></span></li> <li><a href="@Url.Content("~/StoreManager/")">Admin</a></li> </ul> </div> <!-- skipped --> <script> $('#shopping-cart').load('@Url.Action("CartSummary", "ShoppingCart")'); </script> 


In the controller code:
 //[ChildActionOnly] // [HttpGet] // public ActionResult CartSummary() { var cart = ShoppingCart.GetCart(this.HttpContext); ViewData["CartCount"] = cart.GetCount(); this.Response.Cache.SetCacheability(System.Web.HttpCacheability.NoCache); //  return PartialView("CartSummary"); } 


Setting the NoCache caching mode is necessary, as browsers can by default cache Ajax requests.

In itself, such a transformation makes the application only slower. According to the test results - 52 pages per second, taking into account the ajax request to get the status of the basket.

Overclocking the application


Now you can tighten the lazy caching. The main page itself can be cached everywhere and for quite a long time (statistics suffers errors).
To do this, you can simply attach the OutputCache attribute to the controller method:
 [OutputCache(Location=System.Web.UI.OutputCacheLocation.Any, Duration=60)] public ActionResult Index() { // skipped } 


In order for it to work successfully when compressing dynamic content, you need to add a parameter to your web.config:
 <system.webServer> <urlCompression dynamicCompressionBeforeCache="false"/> </system.webServer> 

This is necessary so that the server does not give the Vary: * header, which actually disables caching.

Load testing showed a result of 197 pages per second. In fact, the home \ index page was always returned from the user’s or server’s cache, that is, as fast as possible, and the test measured the speed of the ajax request that received the number of items in the basket.

To speed up the basket you need to do a little more work. To begin with, the result of cart.GetCount () can be saved in the asp.net cache, and reset the cache when the number of items in the basket changes. It turns out in some way write-through cache.

In the MVC Music Store, it is very easy to make such caching, as only 3 actions change the state of the basket. But in a difficult case, most likely, it will be necessary to implement the publish \ subscribe mechanism in the application in order to centrally manage the reset of the cache.

The method of obtaining the number of elements:
 [HttpGet] public ActionResult CartSummary() { var cart = ShoppingCart.GetCart(this.HttpContext); var cacheKey = "shooting-cart-" + cart.ShoppingCartId; this.HttpContext.Cache[cacheKey] = this.HttpContext.Cache[cacheKey] ?? cart.GetCount(); ViewData["CartCount"] = this.HttpContext.Cache[cacheKey]; return PartialView("CartSummary"); } 


In the methods that change the basket, you need to add two lines:
 var cacheKey = "shooting-cart-" + cart.ShoppingCartId; this.HttpContext.Cache.Remove(cacheKey); 


As a result, the load test shows 263 requests per second. 4 times more than the original version.

We use HTTP caching


Last chord - screwing HTTP caching to the request of the number of items in the basket. For this you need:
  1. Give Last-Modified in response headers
  2. Handle If-Modified-Since in Request Headers (Conditional GET)
  3. Give code 304 if the value has not changed


Let's start from the end.
ActionResult code for Not Modified response:
 public class NotModifiedResult: ActionResult { public override void ExecuteResult(ControllerContext context) { var response = context.HttpContext.Response; response.StatusCode = 304; response.StatusDescription = "Not Modified"; response.SuppressContent = true; } } 


Add Conditional GET processing and Last-Modified installation:
 [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<DateTime, int>)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"); } 


Of course, such code in production cannot be written, it is necessary to split it into several functions and classes for ease of maintenance and reuse.

The final result on the one-minute race is 321 pages per second, 5.3 times higher than in the original version.

Interruption


In a real project, you need to design a web application from the very beginning, taking into account caching, especially HTTP caching. Then it will be possible to withstand heavy loads on a rather modest iron.

Source: https://habr.com/ru/post/168869/


All Articles