TOC

The community is working on translating this tutorial into Russian, but it seems that no one has started the translation process for this article yet. If you can help us, then please click "More info".

Caching:

In-memory caching with IMemoryCache

As we saw in the previous article, the concept of OutputCache was introduced in previous versions of the .NET framework and its also supported in the .NET Core framework through the use of a NuGet package. However, in .NET Core 2.0, Microsoft introduced a more native approach to in-memory caching with the Microsoft.Extensions.Caching.Memory namespace.

This means that you can now use caching on the server natively, without the use of NuGet packages, in your ASP.NET Core projects. This is most commonly done in your Controllers, using Dependency Injection. So, for instance, if you want to use caching in your HomeController, you simply need to include the Microsoft.Extensions.Caching.Memory namespace through a using-statement and then inject the IMemoryCache service into the constructor of the Controller. Here's an example:

....
using Microsoft.Extensions.Caching.Memory;

namespace MemoryCacheSample.Controllers
{
    public class HomeController : Controller
    {
        private IMemoryCache _memoryCache;
        
        public HomeController(IMemoryCache memoryCache)
        {
            this._memoryCache = memoryCache;
        }
        ....

You can now use the _memoryCache object in your Controller actions. It comes with several handy methods to cache objects and retrieve them again. This is also one of the main differences between OutputCache and the IMemoryCache approach: While the OutputCache was generally used to cache the entire result of a controller method, IMemoryCache is generally used to store the objects needed to generate the result.

So for instance, if you have a UserController with a Details() method, to display details about a user from the database, the OutputCache approach would cache the entire output of this method - with IMemoryCache, you would instead cache the User object and then use it to return the appropriate View. In both situations, you save the potentially expensive database call, but you get more control of the process with the IMemoryCache approach.

Just make sure that you remember the following guidelines when using in-memory caching:

  • Don't assume that your objects are still in the cache - make sure that you always check and provide code for (re)filling the cache
  • Since in-memory caching uses the RAM on your server to store objects you should treat it as a scarce resource and limit the amount and size of stuff you put into it
  • You should also make sure to set an expiration on the data you cache, so that the framework is allowed to remove stuff from the cache again once it's not valid anymore
  • The framework doesn't enforce any size/memory limitations to your cache - you are required to do this manually, using the SetSize, Size, and SizeLimit properties/methods, if your use of the caching mechanism goes beyond basic usage

With that in place, let's try using the MemoryCache - here's a simple example:

public IActionResult Index()
{
    string cacheKey = DateTime.Now.ToString("yyyyMMddHHmm");
    string cachedMessage;

    if(!this._memoryCache.TryGetValue(cacheKey, out cachedMessage))
    {
    	// Create a fake delay of 3 seconds to simulate heavy processing...
        System.Threading.Thread.Sleep(3000);
        
        cachedMessage = "Cache was last refreshed @ " + DateTime.Now.ToLongTimeString();
        this._memoryCache.Set(cacheKey, cachedMessage);
    }

    return Content(cachedMessage);
}

The first thing we do, is to create a cache-key. It will be used to store our value in the cache, as well as retrieve it again. In our example, our key is generated with a DateTime string consisting of the date, month, year, hour and minute. So each time, we use the TryGetValue() method to try and get an object from the cache which is stored within the current minute - if it doesn't exist, the TryGetValue() method will return false and we'll create the object (in this case, just a string) and then store it in the cache using the Set() method.

To simulate the process of e.g. fetching a lot of data to be cached, from a database or a network resource, we have added a 3 second delay using the Sleep() method. At the end of our action, we return the cachedMessage (which has either just been generated or fetched from the cache) as a simple string, using the Content() method.

If you test this, you will get output like this:

Cache was last refreshed @ 09:58:36

Try reloading a couple of times and you will see that the timestamp only changes when the minute changes, because of the way we generate the cache-key. You will also notice a major difference in how long the request takes, thanks to our fake delay when filling the cache - it will take around 3 seconds when filling the cache, but it will be almost instantaneous when we can get the value from the cache.

Cache expiration

As mentioned previously, it's always a good idea to set an expiration date/time on stuff you cache. If you don't, it won't be removed from the cache (unless the application pool is recycled) and while that is okay for some use-cases, most of your data should probably expire sooner or later, so that it can be refreshed e.g. from the database. Fortunately for us, ASP.NET makes it easy for us to specify when cached data should expire. There are two types of expiration that you can set for your cached entries: Sliding and Absolute.

Sliding expiration

A sliding expiration will allow a cached entry to expire if it hasn't been accessed for a defined amount of time. A common usage for this would be to load something heavy and then set a sliding expiration of e.g. 15 minutes: Each time the user accesses the data, the expiration will be postponed, allowing the user to keep on working with the data. However, when 15 minutes have passed without the user accessing the cached data, it will expire and the resources will be freed.

In the next example, I'll show you how to set a sliding expiration. It will be based on our first example in this article, but you'll notice that I use another approach for accessing/filling the cache. The method is called GetOrCreate() and it's very convenient - you simply specify a cache-key and then a method for filling the cache in case nothing is found with the key. The method will include a parameter which you can also use for specifying options for the cache entry and it will return the cached entry, no matter if it's actually pulled from the cache right now or created instead. Here's how it looks:

public IActionResult GetOrCreateWithSlidingExpiration()
{
    string cacheKey = DateTime.Now.ToString("yyyyMMdd");
    
    string cachedMessage = this._memoryCache.GetOrCreate(cacheKey, entry =>
    {                
        // Create a fake delay of 3 seconds to simulate heavy processing...
        System.Threading.Thread.Sleep(3000);

        entry.SetSlidingExpiration(TimeSpan.FromSeconds(30));

        return "Cache was last refreshed @ " + DateTime.Now.ToLongTimeString();
    });

    return Content(cachedMessage);
}

First of all, you'll notice that I have changed the cacheKey string. It now consists of the just the date (not time), basically meaning that the cached entry will be valid as long as the date doesn't change. However, when filling the cache, I specify a sliding expiration of 30 seconds, using the SetSlidingExpiration() method. Try this new action in your browser and reload it several times. You will notice that the cached entry remains the same as long as less than 30 seconds have passed since your last request - as soon as you let more than 30 seconds pass, the cache entry is removed and it will be filled with a new entry upon your request.

Absolute expiration

The other type of expiration is called Absolute expiration. It can be used as an alternative, or as an addition, to the sliding expiration. With an absolute expiration in place, the cache entry will expire at a pre-defined time, no matter how many times it has been accessed in the meantime. As you can see from the next example, you can use it in the same way as if setting a sliding expiration:

public IActionResult GetOrCreateWithAbsoluteExpiration()
{
    string cacheKey = DateTime.Now.ToString("yyyyMMdd");

    string cachedMessage = this._memoryCache.GetOrCreate(cacheKey, entry =>
    {
        // Create a fake delay of 3 seconds to simulate heavy processing...
        System.Threading.Thread.Sleep(3000);

        entry.SetAbsoluteExpiration(TimeSpan.FromSeconds(120));

        return "Cache was last refreshed @ " + DateTime.Now.ToLongTimeString();
    });

    return Content(cachedMessage);
}

In this example, the cache entry will expire after 120 seconds (2 minutes) no matter if the cache entry is accessed within these 2 minutes or not.

As mentioned, you can combine a sliding and an absolute expiration. This is generally a good idea when you use a sliding expiration, to make sure that it actually DOES expire at some point, even if users keeps accessing it before the sliding expiration is reached.

Summary

As you can see from the above examples, working with IMemoryCache in ASP.NET Core is both easy and efficient. Caching objects can be a very powerful tool when you need to make your web application faster and it can save your database server from a lot of work. Just make sure that you put some thought into how much you can cache and for how long, so it doesn't hurt the user experience.


This article has been fully translated into the following languages: Is your preferred language not on the list? Click here to help us translate this article into your language!