What is Response Caching?
Caching refers to the technique of persisting frequently accessed data in a high-speed memory and querying the memory for all subsequent requests.
Response Caching is a technique of caching the response data based on a unique selector – generally request paths.
In this technique an application server does not need to process and generate a response for the same request again since the response is served from the cache. This results in reduced processing load on the servers and improved turnaround times for clients.
When NOT to cache Responses
As the name suggests, Response Caching results in returning cached responses to requesting clients. This can create ambiguity and conflicts when we try to cache responses of frequently changing entities.
Also, it is recommended to ONLY CACHE GET REQUESTS and NOT any other actions, as caching any other actions can result in conflict between data in cache and in the back-end.
Different ways to cache Responses
Broadly speaking, there are different ways in which we can cache responses.
- Response Caching via HTTP Headers
- In this approach, the server generates a set of response cache headers along with the response. The data is cached on the client’s end within the browser.
- ASP.NET Core provides a ResponseCachingMiddleware to achieve this in an easier way.
- In-Memory Caching
- In this approach, the response content generated for a given identifier is cached in-memory, on the server side. This cached content is served for all subsequent calls.
- In ASP.NET Core MVC and Razor Views, we can use the built-in <cache> tags to define the sections of Views that are to be cached and reused.
- Since the content is persisted in-memory, the caches are cleared once the application restarts.
- Distributed Caching
- In this approach, the response content is cached in a distributed cache system external to the application servers.
- This approach is most suitable for distributed applications, where the cache block is externalized and requests may be served by more than one server.
- ASP.NET Core provides <distributed-cache> tag helpers to cache content based on a unique selector. These Tag Helpers use an implementation of IDistributedCache to cache and retrieve content.
- Since the data is stored externally, it is independent of the server status and restarts.
- ASP.NET Core has two built-in implementations of IDistributedCache – SQL Server, Redis. In this article, we will focus on implementation using NCache
Caching with Tag Helpers
As mentioned before, dotnetcore provides support for view side caching via Cache tag helper. A Cache Tag Helper provides the ability to improve the performance of your ASP.NET Core app by caching its content to the internal ASP.NET Core cache provider.
The first request to the page that contains the Tag Helper displays the latest content that is rendered from the data source.
Additional requests show the cached value until the cache expires (default 20 minutes) or until the cached entry is evicted from the cache. Cache Tag helper stores data in-memory, which isn’t efficient in applications deployed over Web Farms.
A Distributed Cache Tag Helper provides the ability to dramatically improve the performance of your ASP.NET Core app by caching its content to a distributed cache source. An instance of IDistributedCache implementation is passed into the Distributed Cache Tag Helper’s constructor.
How to use NCache for Distributed Response Caching
To use NCache as a distributed cache for response caching, NCache provides its own extension methods to configure services and middleware. To get started, install the below package into NCachedBookStore.Web project.
> dotnet add package NCache.Microsoft.Extensions.Caching
The version of the ResponseCache package matches with the version of NCache installation being used. In my case, I’m using NCache 5.3 in my system for testing, so I install the latest.
Once the package is installed, we register the NCache Distributed Caching service in the Startup class.
services.AddResponseCaching();
//Add NCache services to the container
services.AddNCacheDistributedCache(Configuration.GetSection("NCacheSettings"));
I’m passing the NCacheSettings configuration section to the service. The appsettings.json looks like below:
{
"ConnectionStrings": {
"DefaultConnection": "Data Source=(localdb)\\mssqllocaldb;Database=ncachedbookstore;MultipleActiveResultSets=true"
},
"NCacheSettings": {
"CacheName": "demoCache",
"EnableLogs": "True",
"ExceptionsEnabled": "True"
}
}
The AddNCacheDistributedCache() extension method initializes configurations from appsettings.json before adding the services to the container. It then adds NCache as the default distributed cache as an implementation of IDistributedCache.
Once NCache services have been configured for response caching, we can specify specific content of the views to cache.
For example, the details view is wrapped under the tag as below:
@using NCachedBookStore.Contracts.Entities
@model Book
<div class="row float-lg-right">
<h9 style=" color:red">Page Requested on @DateTime.Now.ToString()</h9>
</div>
<distributed-cache name="Book:@{
@Model.Id
}" expires-on="@DateTime.Now.AddMinutes(5)" vary-by-query="version">
<div class="row float-lg-right">
<h9 style=" color:green">Last generated at @DateTime.Now.ToString()</h9>
</div>
<div class="container-fluid">
<div class="form-group">
<label asp-for="Name">Name</label>
<input type="text" class="form-control" asp-for="Name" disabled />
</div>
<div class="form-group">
<label asp-for="Description">Description</label>
<textarea type="text" class="form-control" asp-for="Description" disabled></textarea>
</div>
<div class="form-group">
<label asp-for="ISBN">ISBN</label>
<input type="text" class="form-control" asp-for="ISBN" disabled />
</div>
<div class="form-group">
<label asp-for="Price">Price</label>
<input type="number" class="form-control" asp-for="Price" disabled />
</div>
<div class="form-group">
<label asp-for="AuthorName">Author Name</label>
<input type="text" class="form-control" asp-for="AuthorName" disabled />
</div>
<a class="btn btn-danger" asp-action="Delete" asp-route-id="@Model.Id">Delete >></a>
<a class="btn btn-info" asp-action="Edit" asp-route-id="@Model.Id">Edit >></a>
</div>
</distributed-cache>
In this case, the block within which renders the details of a particular Book is cached until absolute expiry of value specified in “expires-on” which in this case is 10 minutes from the first visit of the page.
The value of the Last Generated Timestamp doesn’t change until the cache expires.
Every request for the page is now returned from the cache, as we can see that the Last generated time doesn’t change. Also there’s no querying on the database, and we can see spikes in the cache monitor under Fetches
We have passed two additional attributes to the <distributed-cache> tag above, which are “expires-on” and “vary-by-query” apart from the “name” attribute.
The “expires-on” attribute sets an absolute expiry for the block that is being cached. In our case, the cache is invalidated exactly after 5 minutes. We can also put a sliding expiration using “expires-sliding” attribute.
The “vary-by-query” attribute here configures that any variation in the query string key “version” means a new result is obtained and is cached.
How to use NCache for Advanced Distributed Response Caching
Apart from using the built-in cache tag helpers, we can also use tag helpers provided by NCache for advanced level of caching.
NCache provides two additional features on top of the default features provided by ASP.NET Core tag helpers.
- Database Dependency
- When you add database dependency to any page, any change in the database removes the page from the cache. This helps keep the cache data in sync with the database.
- NCache supports two database providers to implement this feature
- SQL Server
- Oracle
- Data Invalidation
- When a data changes, you can mark a particular representation as invalid and force cache to fetch new data for that particular key. This is useful when a Model object changes and the view representation of that model needs to be invalidated in the cache. Examples are entity Edits and Deletes.
To use advanced response caching features, install the below package into NCachedBookStore.Web project.
> dotnet add package AspNetCore.ResponseCache.NCache
The version of the ResponseCache package matches with the version of NCache installation being used. In my case, I’m using NCache 5.3 in my system for testing, so I install the latest.
Once the package is installed, we register the NCache ResponseCaching service in the Startup class.
//Read NCache specific configurations from appsettings.json
services.AddOptions()
.Configure<NCacheConfiguration>(Configuration.GetSection("NCacheSettings"));
//Register NCache for response caching
services.AddNCacheResponseCachingServices();
I’m binding the NCacheSettings configuration section to the IOptions, which is injected wherever required. The appsettings.json looks like below:
{
"ConnectionStrings": {
"DefaultConnection": "Data Source=(localdb)\\mssqllocaldb;Database=ncachedbookstore;MultipleActiveResultSets=true"
},
"NCacheSettings": {
"CacheName": "demoCache",
"DefaultConnectionStringName": "DefaultConnection"
}
}
We will now modify the Details page which contained the <distributed-cache> section. Instead of <distributed-cache> we will now use the NCache custom Tag Helpers <distributed-cache-ncache>
Add the following line to _ViewImports.cshtml. It will then allow the use of NCache Tag helpers.
@using NCachedBookStore.Web
@using NCachedBookStore.Web.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@* NCache Tag Helpers *@
@addTagHelper *, DistributedNCacheResponseCaching
The Details View now looks like below
@using NCachedBookStore.Contracts.Entities
@model Book
<div class="row float-lg-right">
<h9 style=" color:red">Page Requested on @DateTime.Now.ToString()</h9>
</div>
<distributed-cache-ncache name="Book:@{
@Model.Id
}" expires-on="@DateTime.Now.AddMinutes(10)">
<div class="row float-lg-right">
<h9 style=" color:green">Last generated at @DateTime.Now.ToString()</h9>
</div>
<div class="container-fluid">
<div class="form-group">
<label asp-for="Name">Name</label>
<input type="text" class="form-control" asp-for="Name" disabled />
</div>
<div class="form-group">
<label asp-for="Description">Description</label>
<textarea type="text" class="form-control" asp-for="Description" disabled></textarea>
</div>
<div class="form-group">
<label asp-for="ISBN">ISBN</label>
<input type="text" class="form-control" asp-for="ISBN" disabled />
</div>
<div class="form-group">
<label asp-for="Price">Price</label>
<input type="number" class="form-control" asp-for="Price" disabled />
</div>
<div class="form-group">
<label asp-for="AuthorName">Author Name</label>
<input type="text" class="form-control" asp-for="AuthorName" disabled />
</div>
<a class="btn btn-danger" asp-action="Delete" asp-route-id="@Model.Id">Delete >></a>
<a class="btn btn-info" asp-action="Edit" asp-route-id="@Model.Id">Edit >></a>
</div>
</distributed-cache-ncache>
When we run the application, the View is cached and is returned for all subsequent calls.
Implementing Database Dependency
We modify the Index page that shows a grid of all books available as below –
@using NCachedBookStore.Contracts.Entities
@model IEnumerable<Book>
<div class="row">
<distributed-cache-ncache name="booksList" expires-sliding="new TimeSpan(0,10,0)"
depends-on="SELECT [b].[Id], [b].[AddedOn], [b].[AuthorName], [b].[Description], [b].[ISBN], [b].[Name], [b].[Price]
FROM [dbo].[Books] AS [b]"
dependency-type="SQLServer">
<div class="col-md-12"><a class="btn btn-primary" asp-action="Create">Add New</a></div>
@foreach (var item in Model)
{
<div class="col-md-4">
<div class="card bg-light my-2">
<div class="card-body">
<h1 class="card-title">@item.Name</h1>
<p class="card-text">@item.Description</p>
</div>
<div class="card-footer">
<a asp-action="Details" asp-route-id="@item.Id">Details >></a>
</div>
</div>
</div>
}
</distributed-cache-ncache>
</div>
In this View, we use the database dependency feature of NCache, where the result-set of the query is cached and is served for all requests. If any record in database corresponding to query result set modified, the cache data is invalidated automatically.
Implementing Cache Invalidation
On the other hand, we can trigger cache invalidation whenever a book is Edited. The Edit Action is now modified to call another page Success.cshtml where we invalidate the contents of cache for that BookId.
// POST: BooksController/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> EditAsync(int id, BookDto entity)
{
try
{
var book = await _mediator.Send(new UpdateBookCommand(entity, id));
return RedirectToAction("EditSuccess", new { id });
}
catch
{
return View();
}
}
public async Task<IActionResult> EditSuccessAsync(int id)
{
var book = await _mediator.Send(new GetBookByIdQuery(id));
return View("Success", book);
}
@model NCachedBookStore.Contracts.Entities.Book
@*THIS PAGE IS NEVER CACHED. IT INVALIDATES THE CACHED RESPONSE OF Details.cshtml FOR THE GIVEN BookID*@
<distributed-cache-ncache name="Book:@{
@Model.Id
}" invalidates="true">
</distributed-cache-ncache>
<h4>Book details have been updated sucessfully.</h4>
<a [asp-controller]="Home" [asp-action]="Index">Go Home</a>
Conclusion
In this exhaustive guide, we looked into how we can cache responses in ASP.NET Core. We briefly looked into the various ways in which we can cache response content in our applications with the built-in options available – Response Caching by Headers, In-Memory and Distributed. We have also seen about the Tag Helpers that help us in caching parts of Views based on a unique selectors and how they are wired internally.
We then looked into Distributed Caching for Response content and how we can use NCache as an underlying Cache provider to achieve the same. We also looked into the advanced features that NCache offers us on top of the default features – Database Dependency and Cache Invalidation.
NCache offers caching responses via both IDistributedCache and an advanced level of Response Caching via its own Response Caching Tag Helper.
You can implement Distributed Caching for responses via the Nuget package NCache.Microsoft.Extensions.Caching and the advanced Response Caching functionalities via the other Nuget package AspNetCore.ResponseCache.NCache.
Examples for both the implementations have been discussed above.
The code snippets used in this article are are part of the larger NCachedBookStore repository that demonstrates features and how-tos with NCache.
You can pull the code into your machine with the following command –
> git clone https://github.com/referbruv/NCachedBookStore.git -b develop-response-caching