
I believe it has happened to everyone, in our job, to receive requests from clients, or feedback from users of our applications, to improve responsiveness.
If using best practices when we write code it’s not enough, we surely need to use the caching to nudge our applications.
Caching consists in storing somewhere those information, that change less frequently. The frequency is a business requirement of our application.
In this article, we will see what does ASP.NET Core make avalable for caching.
IMemoryCache and IDistributedCache
These two interfaces represent the built-in mechanism for caching in .NET Core. All the other techniques, you may have heard about, are implementations of these two interfaces. In this article, we will look in detail at the in-memory cache, whereas the distributed cache will be examined in a future article.
Enable in-memory caching in ASP.NET Core
The ASP.NET in-memory cache is a feature we can incorporate in our application using the method ConfigureServices. You can enable the in-memory cache in the Startup class as shown in the code snippet below.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddMemoryCache();
}
The AddMemoryCache method allows us to register the IMemoryCache interface that, as mentioned above, is the basis to be used for the caching. Below we see the definition of the interface in the framework:
public interface IMemoryCache : IDisposable
{
bool TryGetValue(object key, out object value);
ICacheEntry CreateEntry(object key);
void Remove(object key);
}
Methods present in the interface are not the only ones available to work with the cache: there are different extensions that enrich the available APIs and greatly facilitate their use, as we will see later. For example:
public static class CacheExtensions
{
public static TItem Get<titem>(this IMemoryCache cache, object key);
public static TItem Set<titem>(this IMemoryCache cache, object key, TItem value, MemoryCacheEntryOptions options);
public static bool TryGetValue<titem>(this IMemoryCache cache, object key, out TItem value);
...
}
Once registered, the interface is injectable in class constructors where we want to use it, as follows:
private IMemoryCache cache;
public MyCacheController(IMemoryCache cache)
{
this.cache = cache;
}
In the section that follows, we will look at how we can work with the cache API in ASP.NET Core to store and retrieve objects.
Store and retrieve items using IMemoryCache
To write an object using the IMemoryCache interface, use the Set<T>() method as shown in the following snippet.
[HttpGet]
public string Get()
{
cache.Set(“MyKey”, DateTime.Now.ToString());
return “This is a test method...”;
}
This method accepts two parameters, the first is the key, with which the cached object will be identified, the second parameter is the value we want to store. To retrieve an object from the cache the Get<T>() method is used as shown with the next snippet.
[HttpGet(“{key}”)]
public string Get(string key)
{
return cache.Get<string>(key);
}
If we are not sure that a specific key is present in our cache, the TryGetValue() method comes to help: it returns a Boolean, that indicates the existence or non-existence of the requested key.
Below is how you can modify the Get() method using the TryGetValue.
[HttpGet(“{key}”)]
public string Get(string key)
{
string obj;
if (!cache.TryGetValue<string>(key, out obj))
obj = DateTime.Now.ToString();
cache.Set<string>(key, obj);
return obj;
}
Another method available is the GetOrCreate() method, that verifies the existence of the required key, otherwise, the method creates it for you.
[HttpGet(“{key}”)]
public string Get(string key)
{
return cache.GetOrCreate<string>(“key”,
cacheEntry => {
return DateTime.Now.ToString();
});
}
How to set expiration policies on cached data in ASP.NET Core
When we store objects with IMemoryCache, the class MemoryCacheEntryOptions provides us various techniques to manage the expiration of cached data.
We can indicate a fixed time after which a certain key expires (absolute expiry), or it can expire if it is not accessed after a certain time (sliding expiry). Furthermore, there is the possibility to create dependencies between cached objects using the Expiration Token. Here some examples
//absolute expiration using TimeSpan
_cache.Set("key", item, TimeSpan.FromDays(1));
//absolute expiration using DateTime
_cache.Set("key", item, new DateTime(2020, 1, 1));
//sliding expiration (scade se non viene letta per 7 giorni)
_cache.Set("key", item, new MemoryCacheEntryOptions
{
SlidingExpiration = TimeSpan.FromDays(7)
});
//use both absolute and sliding expiration
_cache.Set("key", item, new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(30),
SlidingExpiration = TimeSpan.FromDays(7)
});
// This method adds a trigger to refresh the data from background
private void UpdateReset()
{
var mo = new MemoryCacheEntryOptions();
mo.RegisterPostEvictionCallback(RefreshAllPlacessCache_PostEvictionCallback);
mo.AddExpirationToken(new CancellationChangeToken(new CancellationTokenSource(TimeSpan.FromMinutes(35)).Token));
Cache.Set(CACHE_KEY_PLACES_RESET, DateTime.Now, mo);
}
// Method triggered by the cancellation token that triggers the PostEvictionCallBack
private async void RefreshAllPlacesCache_PostEvictionCallback(object key, object value, EvictionReason reason, object state)
{
// Regenerate a set of updated data
var places = await GetLongGeneratingData();
Cache.Set(CACHE_KEY_PLACES, places, TimeSpan.FromMinutes(40));
// Re-set the cache to be reloaded in 35min
UpdateReset();
}
Cache callbacks
Another interesting feature available with MemoryCacheEntryOptions class is the one that permits us to register callbacks, to be executed when an item is removed from the cache.
MemoryCacheEntryOptions cacheOption = new MemoryCacheEntryOptions()
{
AbsoluteExpirationRelativeToNow = (DateTime.Now.AddMinutes(1) - DateTime.Now),
};
cacheOption.RegisterPostEvictionCallback(
(key, value, reason, substate) =>
{
Console.Write("Cache expired!");
});
Cache Tag Helper
We have seen so far the use of APIs made available by .NET Core, to be able to manually write and read item in the cache using the IMemoryCache interface directly. There are also other implementations of this interface, that can be very useful. For example, in the Web environment, if we use the .NET Core MVC framework, we can store parts of pages using the helper cache tag. It’s really simple to use: you can wrap part of a view in cache tags to enable caching:
<cache>
<p>Ora: @DateTime.Now</p>
</cache>
For each subsequent request of the page, that contains this tag, the body of the paragraph will be used from the cache. You can easily check the behavior, if you put it on a page and observe its output. Of course, the way we used it is for example purposes only, but you can appreciate its capabilities when you try to render a piece of page, that requires a lot of resources. One obvious candidate for caching is a view component call
<cache expires-on="@TimeSpan.FromSeconds(600)">
@await Component.InvokeAsync("BlogPosts", new { tag = "popular" })
</cache>
In the previous snippet, you can also see how to manage the expiring period of the object in cache, through the attribute expires-on. There are two other alternatives:
- expires-after: to be evaluated with a TimeSpan to indicate a period of time, after which, the content must be regenerated;
- expires-sliding: a TimeSpan that indicates a period of inactivity should also be used. Each time the content is read from the cache, its removal is postponed.
Another customizable aspect concerns the possibility to configure the caching criterion. We may need to update our cached object based on some variables. Some requirements are covered by the vary-by- attributes listed below:
- vary-by-route: is enhanced with the name of a route parameter, for example id, to indicate that the content must be regenerated when the indicated property change;
- vary-by-query: the content is generated and cached when the querystring key is changed;
- vary-by-user: must be set to true when we display specific data for the logged in user, such as the profile box containing the name and photo;
- vary-by-header: to vary the cache based on an HTTP request header, such as “Accept-Language” if we are using it to display language content;
- vary-by-cookie: allows you to change the cache based on the content of a cookie, whose name we must indicate.
It is possible to use one or more vary-by- attributes to carry out advanced caching policies, but, taking up a famous quote, “With great power, comes great responsibility”.
Conclusions
Using caching in-memory allows you to store data in the server’s memory and helps us to improve application performances by removing unnecessary requests to external data sources. As we have seen, it is very simple to use.
I remind you that this approach cannot be used when your app is hosted on multiple servers or in a cloud hosting environment. This aspect will be examined in the next article in which we will talk about distributed caching.
See you next!