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);