10 things to know about in-memory caching in ASP.NET Core
The primary purpose of any caching mechanism is to improve performance of an
application. As an ASP.NET developer you are probably aware that ASP.NET web
forms as well as ASP.NET MVC could used Cache object to cache application data.
This was often called server side data caching and was available as an inbuilt
feature of the framework. Although ASP.NET Core doesn't have Cache object as
such, you can implement in-memory caching quite easily. This article shows you
how.
Before you read any further create a new ASP.NET Core application based on
Web Application project template.
Then follow the steps mentioned below one-by-one to build and test various
features offered by in-memory caching.
1. In-memory caching needs to enabled in the Startup class
Unlike ASP.NET web forms and ASP.NET MVC, ASP.NET Core doesn't have the
built-in Cache object that you can directly used inside controllers. Here,
in-memory caching works through dependency injection and hence the first step is
to register the in-memory caching service in the Startup class. So, open the
Startup class and locate the ConfigureServices() method. Modify the
ConfigureServices() method to look like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddMemoryCache();
}
To add in-memory caching capabilities to your application you need to call
AddMemoryCache() method on the services collection. This way the default
implementation of an in-memory cache - an IMemoryCache object - can be injected
to the controllers.
2. In-memory caching uses dependency injection to inject the cache object
Then open the HomeController and modify it as shown below:
public class HomeController : Controller
{
private IMemoryCache cache;
public HomeController(IMemoryCache cache)
{
this.cache = cache;
}
....
}
As you can see, the above code declares a private variable of ImemoryCache.
This variable gets assigned in the constructor. The constructor receives the
cache parameter through DI and then this cache object is stored in the local
variable for later use.
3. You can use Set() method to store an item in the cache
Once you have IMemoryCache object, you can read and write items or entries to
it. Adding an entry into the cache is quite straightforward.
public IActionResult Index()
{
cache.Set<string>("timestamp", DateTime.Now.ToString());
return View();
}
The above code sets a cache entry in the Index() action. This is done using
Set<T>() method of IMemoryCache. The first parameter to Set() method is a key
name by which the entry will be identified. The second parameter is the value of
the key. In this example we store a string key and string value but you can
store other types (primitive types and custom types) also.
4. You can use Get() method to retrieve an item from the cache
Once you add an item into the cache, you would like to retrieve it elsewhere
in the application. You can do so using the Get() method. The following code
shows how.
public IActionResult Show()
{
string timestamp = cache.Get<string>("timestamp");
return View("Show",timestamp);
}
The above code retrieves a cached item from another action (Show) of the
HomeController. The Get() method specifies the type of the item and its key. The
item, if present, will be returned and assigned to timestamp string variable.
This timestamp value is then passed to Show view.
The Show view simply outputs the timestamp value as shown below:
<h1>TimeStamp : @Model</h1>
<h2>@Html.ActionLink("Go back", "Index", "Home")</h2>
To test what you wrote so far, run the application. Navigate to /Home/Index
first so that timestamp key is assigned. Then navigate to /Home/Show and see
whether timestamp value gets outputted. The following image shows a sample run
of the Show() action.
5. You can use TryGet() to check whether a key is present in the cache
If you observe the previous example, you will find that every time you
navigate to the /Home/Index, a new timestamp is assigned to the cached item.
This is because we didn't put any check to assign the value only if the item
doesn't exists. Many a times you would like to do just that. There are two ways
to perform that check inside Index() action. Both are shown below:
//first way
if (string.IsNullOrEmpty
(cache.Get<string>("timestamp")))
{
cache.Set<string>("timestamp", DateTime.Now.ToString());
}
//second way
if (!cache.TryGetValue<string>
("timestamp", out string timestamp))
{
cache.Set<string>("timestamp", DateTime.Now.ToString());
}
The first way uses the same Get() method you used earlier. However, this time
it is used along with an if block. If the Get() can't find the specified item in
the cache, IsNullOrEmpty() will return true. And only then Set() gets called to
add that item.
The second way is more elegant. It uses TryGet() method to retrieve an item.
The TryGet() method returns a boolean value indicating whether the item was
found or not. The actual item can be pulled out using an output parameter. If
TryGet() returns false, Set() is used to add that entry.
6. You can use GetOrCreate() to add an item if doesn't exist
Sometimes you need to retrieve an existing item from the cache. And if that
item doesn't exist you want it to be added. These two tasks - get if it exist OR
create it if it doesn't - can be accomplished using GetOrCreate() method. The
modified Show() method shows how this can be done.
public IActionResult Show()
{
string timestamp = cache.GetOrCreate<string>
("timestamp", entry => {
return DateTime.Now.ToString(); });
return View("Show",timestamp);
}
The Show() action now uses GetOrCreate() method. The GetOrCreate() method
checks whether timestamp key is present or not. If yes the existing value will
be assigned to the local variable. Otherwise a new entry is created and added to
the cache based on the logic specified in the second parameter.
To test this code run /Home/Show directly without navigating to /Home/Index.
You will still see a timestamp value outputted because GetOrCreate() now adds it
if it isn't already present.
7. You can set absolute and sliding expiration on a cached item
In the preceding examples, a cache item once added remains in the cache
unless it is explicitly removed using the Remove() method. You can also set
absolute expiration and sliding expiration on a cached item. An absolute
expiration means a cached item will be removed an an explicit date and time.
Sliding expiration means a cached item will be removed it is remains idle (not
accessed) for a certain amount of time.
To set either of these expiration policies on a cached item you use
MemoryCacheEntryOptions object. The following code shows how
MemoryCacheEntryOptions can be used.
MemoryCacheEntryOptions options =
new MemoryCacheEntryOptions();
options.AbsoluteExpiration =
DateTime.Now.AddMinutes(1);
options.SlidingExpiration =
TimeSpan.FromMinutes(1);
cache.Set<string>("timestamp",
DateTime.Now.ToString(), options);
The above code from the modified Index() action creates an object of
MemoryCacheEntryOptions. It then sets AbsoluteExpiration property to a DateTime
value one minute from now. It also sets the SlidingExpiration property to one
minute. These values indicate that the item will be removed from the cache after
one minute irrespective of whether it is accessed or not. Moreover, if that item
remains idle for one minute it will be removed from the cache.
Once the AbsoluteExpiration and SlidingExpiration values are set, the Set()
method is used to add an item to the cache. This time the
MemoryCacheEntryOptions object is passed as the third parameter of the Set()
method.
8. You can wire a callback when an item is removed from the cache
At times you may want to be notified whenever a cached item is removed from
the cache. There could be several reasons why an item gets removed from cache.
For example, an item might get removed due to explicit call to Remove() method,
it might get removed because its AbsoluteExpiration or SlidingExpiration values
were reached and so on.
To know when an item is removed from the cache you need to wire a callback
function. The following code shows how that is done.
MemoryCacheEntryOptions options =
new MemoryCacheEntryOptions();
options.AbsoluteExpiration =
DateTime.Now.AddMinutes(1);
options.SlidingExpiration =
TimeSpan.FromMinutes(1);
options.RegisterPostEvictionCallback
(MyCallback, this);
cache.Set<string>("timestamp",
DateTime.Now.ToString(), options);
The above code is quite similar to the previous example in that it uses
MemoryCacheEntryOptions to configure the AbsoluteExpiration and
SlidingExpiration. More importantly it also calls the
RegisterPostEvictionCallback() method to wire a callback function just
discussed. In this case the callback function name is MyCallback. The second
parameter is a state object that you wish to pass to the callback function. Here
we pass the HomeController instance (this points to the current HomeController
object) as the state.
The MyCallback function mentioned looks like this:
private static void MyCallback(object key, object value,
EvictionReason reason, object state)
{
var message = $"Cache entry was removed : {reason}";
((HomeController)state).
cache.Set("callbackMessage", message);
}
Observe this code carefully. The MyCallback() is a private static function
inside the HomeController class. It has four parameters. The first two
parameters represent the key and value of the cached item that was just removed.
The third parameter indicates the reason why the item was removed. The
EvictionReason is an enumeration and holds various possible reasons such as
Expired, Removed and Replaced.
Inside the callback function we just form a string message based on the
reason of removal. We want to set this message as another cache item. This needs
access to the cache object of the HomeController. That's where the state
parameter can be used. Using the state object you can get hold of the
HomeController's cache object and Set() a callbackMessage cache item.
The callbackMessage can be accessed from the Show() action like this:
public IActionResult Show()
{
string timestamp = cache.Get<string>("timestamp");
ViewData["callbackMessage"] =
cache.Get<string>("callbackMessage");
return View("Show",timestamp);
}
And finally it can be displayed on the Show view:
<h1>TimeStamp : @Model</h1>
<h3>@ViewData["callbackMessage"]</h3>
<h2>@Html.ActionLink("Go back", "Index", "Home")</h2>
To test the callback, run the application and navigate to /Home/Index. Then
navigate to /Home/Show and refresh the browser from time to time. At some point
the timestamp item will expire due to its AbsoluteExpiration setting. And you
will see the callbackMessage like this:
9. You can set priority for a cached item
Just as you can set the expiration policies of a cached item, you can also
assign a priority value to a cached item. If server experiences shortage of
memory based on this priority the items will be removed to reclaim the memory.
To set the priority you use MemoryCacheEntryOptions again.
MemoryCacheEntryOptions options =
new MemoryCacheEntryOptions();
options.Priority = CacheItemPriority.Normal;
cache.Set<string>("timestamp",
DateTime.Now.ToString(), options);
The Priority property of MemoryCacheEntryOptions allows you to set a priority
value for an item using CacheItemPriority enumeration. Possible values are Low,
Normal, High and NeverRemove.
10. You can set a dependency between multiple cached items
You can also set a dependency between a set of cached items such than when an
item is removed all the dependent items are also removed. To see how this works,
modify the Index() action as shown below:
public IActionResult Index()
{
var cts = new CancellationTokenSource();
cache.Set("cts", cts);
MemoryCacheEntryOptions options =
new MemoryCacheEntryOptions();
options.AddExpirationToken(
new CancellationChangeToken(cts.Token));
options.RegisterPostEvictionCallback
(MyCallback, this);
cache.Set<string>("timestamp",
DateTime.Now.ToString(), options);
cache.Set<string>("key1", "Hello World!",
new CancellationChangeToken(cts.Token));
cache.Set<string>("key2", "Hello Universe!",
new CancellationChangeToken(cts.Token));
return View();
}
The code begins by creating a CancellationTokenSource object and the object
is stored as an independent cached item cts. Then MemoryCacheEntryOptions object
is created as before. This time AddExpirationToken() method of
MemoryCacheEntryOptions is called to specify an expiration token. We won't go
into the details of CancellationChangeToken here. It is suffice to say that an
expiration token allows you to expire an item. If the token is active the item
stays in the cache but if the token is cancelled the item is removed from the
cache. Once the item is removed from the cache MyCallback is called as before.
Then the code creates two more items - key1 and key2. While adding these two
items the third parameter of Set() passes a CancellationChangeToken based on cts
object created earlier.
That means here we have three keys - timestamp is the primary key, key1 and
key2 are dependent on timestamp. When timestamp is removed key1 and key2 should
also get removed. To remove timestamp you need to cancel its token somewhere in
the code. Let's do that in a separate action - Remove().
public IActionResult Remove()
{
CancellationTokenSource cts =
cache.Get<CancellationTokenSource>("cts");
cts.Cancel();
return RedirectToAction("Show");
}
Here we retrieve the CancellationTokenSource object stored earlier and call
its Cancel() method. Doing so will remove timestamp, key1 as well as key2. You
can confirm that by retrieving all these three keys in the Show() action.
In order to test this example, run the application and navigate to
/Home/Index. Then navigate to /Home/Show and check whether all the three key
values are being shown as expected. Then navigate to /Home/Remove. You will be
redirected back to /Home/Show. Since Remove() cancelled the token all the keys
will be removed and now the Show view will show the reason for expiration
(TokenExpired) like this:
That's it for now! Keep coding!!