I’ve been giving the following talk at Meetups, User Groups, and at Tech Conferences.
Ever wonder how you might use caching to improve the performance and speed of your website? Are you looking to improve the user experience for your web application? We’ll discuss the ins and outs of caching in .NET Core, when and where to apply a caching strategy, and considerations for each scheme.
What is caching?
In computing, a cache is a hardware or software component that stores data so future requests for that data can be served faster; the data stored in a cache might be the result of an earlier computation, or the duplicate of data stored elsewhere.
– Wikipedia
Benefits of caching
So, why would you want to implement caching? To boost performance, improve the user experience, and to save money.
Save server roundtrips
If the content that you are delivering never changes or rarely changes, it might be beneficial to let the client cache the information on their machine.
Decrease response time
Sometimes poor performance can be blamed on network latency. Many times this is out of our control for external/public facing websites. Of course you can look into CDNs or hosting your application in different regions around the world. Another potential win is to lessen the time spent on your servers. If your application requires multiple or frequent roundtrips to the database, it may be possible to move the data closer by utilizing some method of caching.
Improve performance
By using a caching mechanism to minimize round trips to the database, for instance, your server(s) will have more resources available to fill more requests and process more information. This can often have a positive impact on performance of an application.
Save money
In there era of cloud computing we’re often charged by computing cycles. A poorly designed application and for that matter a well designed application can squander money by not utilizing a good caching strategy.
Best Practices
Caching can be abused. I’ve had to work on a legacy application that was suffering as a result of their caching strategy. In this application nearly the entire database was loaded into the server’s cache upon login of a new user. Each time a new user would login the cache was invalidated and loaded again. Don’t do this. While appreciate what the prior developers were trying to do, it ended up causing more problems than it solved. Evaluate your application to see how, when, and why to cache.
Cache early, cache often
If you’re working on an application that has a set of lookup data that is used often, but rarely changes, this is a good candidate for caching. Think list of States and State Abbreviations. If could make sense to add a list of warehouses or fulfillment facilities to cache if they are used often enough and are unlikely to change. In these scenarios try to determine the best time in the application lifecycle to populate the cache. Can you add to cache during application start? Would it be acceptable to the first user of the system to incur the cost of populating the cache? There is a small penalty to populating cache, but the benefit of using the cached values should far outweigh said penalty.
Caches should be relatively small
That said, caches should be small. Don’t pull in the entire database if you simply need a list of available shirt sizes. Keep the size and quantity of caches down to a minimum to be useful and manageable.
Expire Cache
If the data being cached has the potential to be modified make sure that there is a way to invalidate or expire the cache. It may be perfectly acceptable to cache product data for up to 30 minutes. Many caching mechanisms will have the ability to invalidate, update, clear, or delete items from the cache. Be sure to use these appropriately.
Let’s look at some code!
We’ll look at a variety of caching mechanism available to us in .NET Core. Examples include ResponseCache, MemoryCache, and touching on a simple way to use Redis for caching.
Response Cache
To get started let’s look at caching on the client. By adding the ResponseCache attribute to an ActionResult you can effectively tell the client’s browser that it is safe to cache on the client side.
In order to use the ResponseCache attribute you must first install the following NuGet packages:
Microsoft.AspNetCore.ResponseCaching
Microsoft.AspNetCore.ResponseCaching.Abstractions
We also need to modify Startup.cs to include Response Caching:
services.AddMvc(); services.AddResponseCaching();
No we can add the ResponseCache attribute to our controller:
[ResponseCache(Duration = 20)] public ActionResult Detail(int id) { var horse = _horseService.Get(id); var model = _horseDetailMapper.Map(horse); ViewBag.Message = "Client Cached"; return View("Detail", model); }
This will add a value to the Response Header that tells the client browser it is safe to store the page in cache for 20 seconds.
Memory Cache
In prior versions of .NET there was an OutputCache attribute that you could apply. This would store the cache on the web server. This has not made it in .NET Core. Instead, we’ll be relying on MemoryCache.
To use MemoryCache you must first install the following NuGet packages:
Microsoft.Extensions.Caching.Abstractions
Microsoft.Extensions.Caching.Memory
We now need to add MemoryCache to Startup.cs:
services.AddMvc(); services.AddMemoryCache();
Now we can implement MemoryCache in our controller:
using System; using System.Collections.Generic; using Example.Services.Interfaces; using Example.Web.Interfaces; using Microsoft.AspNetCore.Mvc; using System.Linq; using Microsoft.Extensions.Caching.Memory; namespace Example.Web.Controllers { public class MemoryCachedController : Controller { private readonly IMemoryCache _memoryCache; private readonly IHorseService _horseService; private readonly IMapper<Dto.Horse, Models.HorseSummary> _horseSummaryMapper; public MemoryCachedController(IMemoryCache memoryCache, IHorseService horseService, IMapper<Dto.Horse, Models.HorseSummary> horseSummaryMapper) { _memoryCache = memoryCache; _horseService = horseService; _horseSummaryMapper = horseSummaryMapper; } public ActionResult Index() { const string cachekey = "CONTROLLER_CACHED"; List<Models.HorseSummary> cached; if (!_memoryCache.TryGetValue(cachekey, out cached)) { // Get the values to be cached var horses = _horseService.GetAll(); cached = horses.Select(_horseSummaryMapper.Map).ToList(); // Decide how to cache it var opts = new MemoryCacheEntryOptions { SlidingExpiration = TimeSpan.FromSeconds(20) }; // Store it in cache _memoryCache.Set(cachekey, cached, opts); } ViewBag.Message = "Memory Cached"; return View("Horses", cached); } } }
Here you can see that if the cache contains data with the supplied key then the cached data is returned. If no cache value is contained with the given key then data is retrieved from the service, added to the cache, and then returned.
Redis
In the following example we’ll be placing our session data in Redis. First, we’ll need to install a Redis server locally using NuGet and ensure it is running:
Redis-64
We next need to install the following NuGet packages:
Microsoft.Extensions.Caching.Redis.Core Microsoft.AspNetCore.Session
We now need to add MemoryCache to Startup.cs:
services.AddMvc(); services.AddDistributedRedisCache(options => { options.InstanceName = "Example"; options.Configuration = "localhost"; }); services.AddSession(); ... app.UseStaticFiles(); app.UseSession();
Now we can interact with Redis from our controller:
using System.Collections.Generic; using Example.Services.Interfaces; using Example.Web.Interfaces; using Microsoft.AspNetCore.Mvc; using System.Linq; using System.Text; using Newtonsoft.Json; namespace Example.Web.Controllers { public class RedisCachedController : Controller { private readonly IHorseService _horseService; private readonly IMapper<Dto.Horse, Models.HorseSummary> _horseSummaryMapper; public RedisCachedController(IHorseService horseService, IMapper<Dto.Horse, Models.HorseSummary> horseSummaryMapper) { _horseService = horseService; _horseSummaryMapper = horseSummaryMapper; } public ActionResult Index() { const string cachekey = "REDIS_CACHED"; byte[] cached; string value; ViewBag.Message = "Redis Cached"; if (HttpContext.Session.TryGetValue(cachekey, out cached)) { // Get the value from cache value = Encoding.UTF8.GetString(cached); ViewBag.Message = "Redis Cached - From Cache"; } else { // Get the values to be cached var horses = _horseService.GetAll(); var mappedHorses = horses.Select(_horseSummaryMapper.Map).ToList(); value = JsonConvert.SerializeObject(mappedHorses); var encodedHorses = Encoding.UTF8.GetBytes(value); HttpContext.Session.Set(cachekey, encodedHorses); } var model = JsonConvert.DeserializeObject<List<Models.HorseSummary>>(value); return View("Horses", model); } } }
With Redis we get the additional benefit of distributed caching. That means that if we had multiple web-servers there wouldn’t be any issues with often encountered with non-sticky sessions, etc.
Caveats
The code examples provide here and at GitHub are used as part of a presentation. There are specific items used for teaching purposes that you should avoid in your own code (hard-coded keys, in-memory filtering, etc.)
Further Reading
Response Caching and In Memory Caching in ASP.NET Core 1.0
Introduction to in-memory caching in ASP.NET Core
Distributed Cache using Redis and ASP.NET Core
Take a look at the sample project on GitHub.
###
Edit: Steve Smith was kind enough to share a post via Twitter the last time I gave this talk that he wrote a while ago that pairs nicely with this one.
An International Speaker, Author, and Microsoft MVP, John has been a professional developer since 1999. He has focused primarily on web technologies and currently focuses on C# and .NET Core in Azure. Clean code and professionalism are particularly important to him, as well as mentoring and teaching others what he has learned along the way.