Caching in Razor Pages

Server-side caching is primarily used to improve performance in applications, but can also be used in web applications as a state management strategy. This type of caching involves the storage of data or content on the server, particularly data or content that incurs a processing overhead, so that it can be reused without incurring the cost of generation every time. The best candidate for caching is data that doesn't change very often. Look-up data, tag clouds, menu-driven navigation are all classic examples that can benefit from caching.

In-memory storage is most likely to be used in applications hosted on a single server. Applications deployed to the cloud or on web farms are recommended to use a distributed cache system - one which is separate to the application so that each instance of the application can access the same data.

Razor Pages offers two main ways in which you can manage the caching of pieces of content. One is to use the Cache tag helper. The other is to use the a cache provider's API such as that offered by the IMemoryCache.

In-Memory Caching

In-memory caching is enabled as a service. It can be injected into PageModel and other classes via their constructor as an instance of IMemoryCache:

public class IndexModel : PageModel
{
    private readonly IMemoryCache _cache;

    public IndexModel(IMemoryCache cache)
    {
        _cache = cache;
    }
    ...
}

Managing Cache Entries

The MemoryCache holds items as Key/Value pairs. You would normally use strings as key values because they are easy to compare when performing look-ups. Comparison is case-sensitive when using strings. The recommendation is to use constants for key values to prevent typographical errors.

The IMemoryCache interface defines three methods for managing cache entries:

  • CreateEntry
  • Remove
  • TryGetValue

The implementation of the CreateEntry method offered by the MemoryCache class is not particularly intuitive to use. It takes the key name as a parameter and returns an ICacheEntry instance that can then have its properties set. ICacheEntry implements IDisposable. The entry is only added to the cache when its Dispose method is called. This can be done in a using block:

using(var cacheEntry = _cache.CreateEntry("myKey"))
{
    // set properties
} // Dispose called at end of block, committing item to the cache

Or you can call the Dispose explicitly:

var cacheEntry = _cache.CreateEntry("myKey");
// set properties
cacheEntry.Dispose(); //adds item to cache

A number of extension methods on the MemoryCache class are made available to simplify setting and getting memory cache values:

Method Return Type Notes
Get object Gets the item with the specified key
Get<TItem> TItem Gets the item with the specified key and attempts to cast it to the specified type
GetOrCreate<TItem> TItem If the item doesn't exist in the cache, this method will add it
Task<TItem> GetOrCreateAsync Task<TItem> Async version of above
Set<TItem> TItem Also has 4 overloads that allow various options to be set
TryGetValue<TItem> bool Generic version of the interface method

Setting values

The simplest way to add an entry to the cache is to use the Set<TItem> method:

var dt = DateTime.Now;
_cache.Set<DateTime>("Time", dt);

More commonly, you will omit the type parameter because it can be inferred from the type that's passed into the Set method:

var dt = DateTime.Now;
_cache.Set("Time", dt);

Overloads of the Set<TItem> method enable you to specify the expiration rules of an item, or a collection of properties wrapped as a MemoryCacheEntryOptions object. These will be examined further when looking at the CacheEntry object's properties.

Getting values

Both the Get and Get<TItem> methods attempt to retrieve a cache entry by the specified key:

var myEntry = _cache.Get("myKey");
var myEntry = _cache.Get<DateTime>("myKey"); 

The first option returns an item of type object or its default value ( null ) if no entry with the specified key exists. The second option returns an item of the type specified in the TItem parameter ( DateTime in this example ) or the default value of TItem if the key doesn't exist. If the cache entry cannot be converted to the type specified by the TItem parameter, an InvalidCastException will be generated.

The TryGetValue method returns a bool to indicate whether retrieval from the cache is successful. The method takes an out parameter for capturing the retrieved value in the event of success:

DateTime dt;
if(_cache.TryGetValue("myKey", out dt))
{
    // the cache entry has been retrieved
}

This method will also generate an InvalidCastException if the item cannot be cast to the specified type. The type parameter can be omitted because the type is inferred from the out parameter.

Finally, the GetOrCreate method will add an entry if one doesn't exist for the specified key:

var o = _cache.GetOrCreate("myKey", entry =>
{
    return DateTime.Now;
});               

In this case o will either contain the value of the cached item with a key of myKey, or if one doesn't exist, the value of o will be DateTime.Now which will be added to the cache with the specified key.

Removing items

Items are explicitly removed from the cache by the Remove method which takes the key as a parameter:

_cache.Remove("myKey");

CacheEntry Properties

The following table details the properties of the CacheEntry class which represents an item stored in the cache:

Property Data Type Description
AbsoluteExpiration DateTimeOffset Gets or sets an absolute expiration date for the cache entry
AbsoluteExpirationRelativeToNow TimeSpan Gets or sets an absolute expiration time, relative to now
SlidingExpiration TimeSpan Gets or sets how long a cache entry can be inactive (e.g. not accessed) before it will be removed. This will not extend the entry lifetime beyond the absolute expiration (if set).
Key object Readonly - gets the key of the item
Value object Gets or sets the value of the item
ExpirationTokens IList<IChangeToken> Gets the IChangeToken instances which cause the cache entry to expire.
Priority CacheItemPriority Gets or sets the priority for keeping the cache entry in the cache during a memory pressure triggered cleanup. Defaults to CacheItemPriority.Normal. Other options are High, Low and NeverRemove
Size long
PostEvictionCallBack IList<PostEvictionCallbackRegistration> Gets or sets the callbacks that will be fired after the cache entry is evicted from the cache.

Managing Expiration Of Cache Entries

Several properties relate to the expiry time of a cache entry. If no value is set for these, the entry will only be removed from the cache if a cleanup takes place that is triggered by memory pressure, or if the process that the application is running within is stopped from some reason (App pool recycle, server shutdown). You should therefore code defensively when use the memory cache. You cannot safely assume that an entry exists, even though you wrote code to add it and tested that the code gets called.

The AbsoluteExpiration property enables you to set the actual time that an item should expire The following example sets the entry to expire just before midnight on News Year's Eve, 2018 regardless when it was added:

using var cacheEntry = _cache.CreateEntry("MyKey");
cacheItem.AbsoluteExpiration = new DateTimeOffset(new DateTime(2018, 12, 31, 23, 59, 59));

Rather than specifying that an entry expires at a particular point in time, you may want to specify a duration, such as 20 minutes from the time that the item is added to the cache. You do this by setting a value for the AbsoluteExpirationRelativeToNow property:

using var cacheEntry = _cache.CreateEntry("MyKey");
cacheItem.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(20);

If both of these properties are set, the item's expiry will be determined by whichever occurs first.

The SlidingExpiration property provides a means to expire infrequently accessed items after a specified period of inactivity:

using var cacheEntry = _cache.CreateEntry("MyKey");
cacheItem.SlidingExpiration = TimeSpan.FromMinutes(20);

This particular item will expire from the cache if it has not been accessed within 20 minutes of it being set or last accessed. Each time it is accessed, the 20 minute time limit will be reset. This will continue to happen until the item is explicitly removed by the Remove method, evicted as a result of memory pressure, or the item has an absolute expiry time set and that time is reached.

You can also set these options via extension methods on an instance of the MemoryCacheEntryOptions class:

var dt = DateTime.Now;
var options = new MemoryCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromMinutes(20));
_cache.Set("Time", dt, options);

The extension methods include:

  • AddExpirationToken
  • RegisterPostEvictionCallback
  • SetAbsoluteExpiration
  • SetPriority
  • SetSize
  • SetSlidingExpiration

Change Tokens and Dependencies

The expiration of items does not need to be set to a specific time. Items can also be expired from the cache as a result of a dependency, such as a database entry, file contents or other source changing - even another cache entry. Dependencies are registered using objects that implement the IChangeToken interface. For more information on using Change Tokens, see Detect changes with change tokens in ASP.NET Core. Tokens are added to the item's ExpirationTokens property. In the next code block, the FileProvider's Watch method returns an IChangeToken, which is used to manage the eviction the cache item in the event of changes to the watched file:

var fileInfo = new FileInfo(@"d:\content\example.txt");
var fileProvider = new PhysicalFileProvider(fileInfo.DirectoryName);
var changeToken = fileProvider.Watch(fileInfo.Name);
cacheItem.ExpirationTokens.Add(changeToken);
Last updated: 2/25/2022 8:16:02 AM

© 2018 - 2024 - Mike Brind.
All rights reserved.
Contact me at Mike dot Brind at Outlook.com