CACHING IN ASP.NET CORE

NET COREAs we have seen connecting to a database takes some time; the better the hardware resources of the server on which the application resides, the more contained this time will be. However powerful a machine may be, there is still a physical limit that will be reached with so many users using the application. When it comes to that there are several solutions to put into practice such as vertical scalability, or horizontal, let’s put two machines so that both can serve users. The solution of scalability works, however, our application requires more resources and therefore more cost. Before we get to that we need to take some steps such as, for example, using the caching service.

Caching

Suppose a user makes a request for a course list, the request arrives at the server which turns it over to the database. Now, however, we can use a little ploy, since there will be multiple users searching the course list we can put this information in the RAM memory of the server where access for the second user is significantly faster. I he problem is that the data may not be updated, but this is rarely because cached objects will expire and we decide how and when to make them expire.

Scadenza cache

Let’s look at the characteristics of the IMemoryCache object.

IMemoryCache

The GetOrCreate method works like this. If the key that identifies the object exists, then it is retrieved from RAM, if it does not exist then we provide a lambda setting validity, 60 seconds in the slide and most importantly we have to provide the instance of the object to be cached. The object can be of any type, even a primitive type. GetOrCreate is Thread safe, that is, it can handle two Threads attempting to access the same object. We see a pattern for implementing the caching service.

Decorator Pattern

This pattern simply indicates that instead of building the caching into the AdoNetCourseService component, i.e. using them IMemoryCache, it is the service we will have to build that goes to encompass AdoNetCourseService, so if a particular object is cached we will get it from there, otherwise we will access the component.

Controller

If in the future we need a CoursesAdminController with updated data, then we will not make it depend on the caching service.

ICachedCourseService

Let’s look at the implementation.

MemoryCachedCourseService
Occupazione Ram

CACHE THE ENTIRE PAGE CONTENT WITH RESPONSE CACHE

ASP.NET Core can also cache the result of the HTML code produced by a View Razor, exactly the HTML code sent to the user’s Browser. From the second request on, the Controller code and its Action will not be executed. To understand how Response Caching works let’s open the Home controller. Our Home Page will soon be viewed by many users, we have every interest that the Home Page content can be put into Cache. We decorate the Index action with ResponseCache, by doing so we are declaring that the View() Razor associated with the Index action can be cached. The duration is specified by Duration, for example 60 seconds, after which the View will be taken again by the server.

Response Cache

Suppose there is a user via his Browser making a request to the path / (the Home). When the request arrives at the server if we are using the ResponseCache attribute, the response is not cached by the server but the server itself adds a Cache-Control: public,max-age=60. This header is gold for the Browser, which by reading it will be able to cache the Home content. When the user visits Home again, it will be drawn from the Browser cache and load instantly. As developers, since the Response-Cache is not in application memory, we cannot invalidate the cache before it expires.

Dispositivi

There may be various devices that cache the response; in fact, it is not necessarily that between user and server there is only the Browser, the question is, what is the purpose of caching the response on all these devices? If the Browser can serve only one user, a Proxy all the employees of a company for example, however if we do not want these intermediate devices to cache the response just use the Location = ResponseCacheLocation.Client attribute. To not use hardwired values in the code we need to write a profile:

[ResponseCache(CacheProfileName =“Home“)]

HomeProfile

Let’s look at the appsettings.json file.

appsettings.json

IMPROVE RESPONSE TIME WITH RESPONSE CACHING MIDDLEWARE

Now we are going to see how thanks to a Middleware of ASP.NET Core it is possible to introduce the cache previously seen, inside our application. This is because the browser cache is only able to improve the user experience from the second time they visit the page, If instead we keep the cache at the server level already from the first response there will be an improvement for all users.

ResponseCaching
Response Caching Middleware

Middleware works in this way. When a request comes from the user for example for the Home Page, the Middleware asks: Can I already give an answer? The answer for the first time is no, in subsequent requests it depends on whether or not there is a Cache-Control: public, max-age=60. Such Middleware acts like one of those devices that stands between user and server as we have seen, in fact the header must be public. Assume that there are all the prerequisites for caching the response, which then returns to the client. The second request always made for the Home Page is served directly by Middleware. In this case the answer is quick because MVC is not involved, the controller the actions.

Pippo

If we make two requests, one http port 5000 and one https port 5001 the requests are different based on the key being calculated, so the same HTML content would be cached twice. This could lead to increased consumption of memory occupied by the cache. It is necessary to make sure that the application can always be reached from an origin. In the second case, the two addresses are the same because there is no reference to the query string in the key.

SOLVE THE PROBLEM IN THE CONFIGURATION

To solve this problem, a VaryByQueryKeys parameter is used in the configuration so that Middleware also takes the query string into account. It is an array where we have indicated only page for the time being. So far everything seems perfect, but there are some flaws; in fact, usually web applications show pages that are not the same for all users, for example, if I logged in as Mario Rossi, another user will always see Mario Rossi, and this is obviously wrong because this is dynamic information; therefore, we have to separate all the information that is the same for all users from the information that is unique to the authenticated user.

Svantaggi
Svantaggi

CACHING SERVICE SUMMARY

The quality of an application is partly determined by its performance: if a user can navigate quickly within our site, he will have fewer obstacles in reaching his goal and therefore his user experience will be positive.

When there are many concurrent users, the performance of our application may deteriorate, especially if we do not have a server with hardware features that can handle the load.

Before upgrading our server hardware, however, we should implement arrangements that allow us to accommodate more users almost “for free.” If we take advantage of ASP.NET Core’s caching service, in fact, we can avoid querying the database with each request and show the user a ready-made result retrieved from the application’s RAM. Thus we will maintain very good performance even with many concurrent users but we must accept the trade-off that this result may not be as up-to-date as possible.

The IMemoryCache service allows us to “save” objects in RAM memory so that they can later be retrieved by a key representing them. This is advantageous because RAM memory is incredibly faster to read than a database.

The objects we will need to consider caching are those that are to be viewed as is by many users, such as our list of courses. In the following example, then, we see how to receive the IMemoryCache service from the constructor of one of our application services.

  1. public class MemoryCacheCourseService : ICachedCourseService.
  2. {
  3. private readonly IMemoryCache memoryCache;
  4. private readonly ICourseService courseService;
  5. //Receive the IMemoryCache service from the constructor
  6. //E so does the application service that allows us to retrieve data from the database.
  7. public MemoryCacheCourseService(IMemoryCache memoryCache, ICourseService courseService)
  8. {
  9. //Conserve references on private fields.
  10. this.memoryCache = memoryCache;
  11. this.courseService = courseService;
  12. }
  13. public Task<List> GetCoursesAsync()
  14. {
  15. //Objects are written to and read from the cache based on a key representing them
  16. string key = “Courses”;
  17. //We invoke the GetOrCreateAsync method to retrieve the object from the cache.
  18. //If it does not exist, then the lambda will be executed, which will deal with
  19. //to retrieve the object from the database and to set a deadline
  20. return memoryCache.GetOrCreateAsync(key, cacheEntry =>
  21. {
  22. //I set the caching deadline (60 seconds from now)
  23. cacheEntry.SetAbsoluteExpiration(TimeSpan.FromSeconds(60));
  24. //Fetch the object using the application service that gets it from the database
  25. //That object will be automatically cached
  26. return courseService.GetCoursesAsync();
  27. });
  28. }
  29. }

By calling the GetOrCreateAsync method we were able, atomically (i.e., with a single operation), to check for the presence in the cache of an object identified by the key “Courses” and, if it was not present, place it in the cache after retrieving it from the database.

Setting an expiration for the object means that it will be automatically removed from the cache when a certain date/time is reached. There are two approaches we can take.

  • Absolute expiration(shown in the example) allows us to indicate a precise date/time at which the object will be removed from the cache;
  • Sliding expiration instead allows us to indicate a deadline (e.g., 60 seconds from now) that will be automatically extended as long as the object continues to be retrieved from the cache.

If we want to remove an object before it expires, we can use the Remove method of the IMemoryCache service.

LIMIT RAM CONSUMPTION

From time to time, we pay attention to the amount of RAM occupied by our application because if we keep too many cached objects we may run the risk of running out of RAM. Eventually we can set limits to prevent too many objects from ending up in the cache, as in the next example, where we set a limit of “1000” from the ConfigureServices method of the Startup class.

  1. services.Configure(options =>
  2. {
  3. options.SizeLimit = 1000;
  4. });

Whenever we add a cached object, let us also remember to indicate its “size.” In this example, the size is set to 1, which means that we can still cache 999 objects before the limit is reached.

  1. //Let’s put this in the lambda passed as a parameter to GetOrCreate
  2. SetSize(1);

RESPONSE CACHING

ASP.NET Core also has a response c aching mechanism that allows browsers or whatever device stands between the server and the user to cache the entire HTML content of a response. This can occur if there is a Cache-Control header in the response provided by the server that gives instructions on how long and how to cache. Here is an example of such a header.

Cache-Control: public,max-age=60

To issue this header in the response, we simply place the ResponseCache attribute at a controller or action, as seen in the following example.

  1. //The response produced by this action can be cached for 60 seconds
  2. [ResponseCache(Duration=60]
  3. public IActionResult Index()
  4. {
  5. ViewData[“Title”] = “Welcome to MyCourse!”
  6. return View();
  7. }

Alternatively, we can make use of a profile, so that caching settings can be reused at multiple points in the application and, more importantly, can be drawn from configuration values rather than being hardwired into C# code.

  1. [ResponseCache(CacheProfileName=”Home”]
  2. public IActionResult Index()
  3. {
  4. ViewData[“Title”] = “Welcome to MyCourse!”
  5. return View();
  6. }

Then, in the ConfigureServices method of the Startup class, we are going to modify the call to AddMvc in this way to define our profile.

  1. services.AddMvc(options =>
  2. {
  3. Configuration.Bind(“ResponseCache:Home”, homeProfile);
  4. options.CacheProfiles.Add(“Home”, homeProfile);
  5. });

And here is the configuration snippet in appsettings.json, from which we get the values.

  1. “ResponseCache”: {
  2. “Home”: {
  3. “Duration”: 60,
  4. “location”: “client”,
  5. “VaryByQueryKeys”: [“page”]
  6. }
  7. }

Each response caching profile is characterized by these values:

  • Duration expresses the duration in seconds of cache time;
  • Location indicates which devices are allowed to cache the result. If set to “Client,” then only the browser is authorized, otherwise if “Any” (the default), any other device is authorized (e.g., an enterprise proxy). If, on the other hand, it is set to “None” then no device is authorized and we are in fact explicitly disabling the entire response caching mechanism;
  • VaryByQueryKeys indicates the names of the querystring varables that will affect page content. So, the browser will not be able to make use of the cache if it changes even one of the given querystring values. Useful when we have paginated lists;
  • VaryByHeader indicates the names of request headers that affect page content. Useful, for example, when from the same address we provide texts translated into the user’s language based on the Accept-Language header indicating the user’s preferred language.

ASP.NET Core also offers a ResponseCachingMiddleware that can cache HTML responses, just as a browser or proxy would. The middleware is thus enabled from the Configure method of the Startup class.

  1. UseStaticFiles();
  2. //Add it AFTER the static file middleware but BEFORE the MVC routing middleware.
  3. UseResponseCaching();
  4. UseMvc(…)

Internally, the middleware makes use of the MemoryCache service we have already discussed.

It is important to remember that the cached result will be the same served to all users, so it should not contain specific information that varies from user to user, such as their username or the contents of their shopping cart. We will return to this aspect by addressing the issue of authentication.

LINK TO CODE ON GITHUB

GITHUB

Download the section12 code or clone the GITHUB repository to have all sections available in your favorite editor.