In this article, we are going to implement Response Caching with MediatR in ASP.NET Core using its awesome Pipeline Behaviours. I have written a couple of articles about MediatR and why itās one of the MUST USE libraries for .NET devs. This library simplifies your Application to an extent yet gives you complete control.
Before getting started with this article, I assume that you are aware of MediatR and its pipeline behaviors. If not, itās recommended that you go through the following articles. Cheers!
- CQRS with MediatR in ASP.NET Core ā Ultimate Guide
- MediatR Pipeline Behaviour in ASP.NET Core ā Logging and Validation
Whatās Response Caching with MediatR?
In a previous article, we discussed the Pipeline Behavior of MediatR and how Logging and Validation of Requests can be implemented. In this post, letās go one step further and implement Caching in the MediatR pipeline. This means that every response that is sent out of your application (API) will be cached by your application with a very simple implementation.
Caching is quite a lifesaver at times for any application. It helps you reduce the round-trip time from the client to server to the database, and probably saves you $$$$ by minimizing the database calls. When it comes to caching in ASP.NET Core, you are presented with a whole lot of options. Few of them are as follows -
However the decision on where the caching happens is quite vital. There are several other questions that can arise when you deal with caching.
- How long should the cache stay?
- Which part of the application flow should be responsible for caching?
- How to bypass cache if needed?
- Do you need to write cache code all throughout the application? or is there a simpler way?
- How to invalidate cache if needed?
MediatR Pipeline Behavior is the answer for all these questions. Given that you are already using CQRS with MediatR, why not use it to its complete potential.
Whenever you send a query to MediatR Handlers, your request and response have to go through an HTTP Pipeline aka MediatR Pipeline. This means, when you send a query request to the application, your request goes into the application scope through the MediatR pipeline. This would be an ideal spot to place your validation logic, right?
Similarly, once the application sends the response back to the client, the response has to cross the MediatR Middleware. Aināt this the ideal spot to place a code that can cache the response? As in, once the response is cached at the Pipeline, any similar request coming into the application will check If the required response is available in the cache-store and try to fetch it. Only if the cache data is not present, the request makes it to your MediatR handlers and then the database. Get it? Oh, the response will have cache-keys attached to it, so that they are unique to requests. For example, cache-keys can be like below -
- customer-1
- customer-99
- customerList
The concept that we saw in the above stanzas could -
- drastically reduce your application response time.
- improve user experience
- less load on the API Server / Database Server
- save you $$$$
What we will build?
To demonstrate Response Caching with MediatR in ASP.NET Core, we will build a .NET 6 WebAPI following a clean architecture. The use case of this application - a simple API with a customers endpoint that can return a list of customers and a single customer detail as well. To keep the implementation shorter, we will not be connecting this implementation to a DataSource, since our main focus is on the caching behavior of MediatR. Instead we will have a static list of customers and try to mimic the delay that can occur with the database response.
Letās get started.
Getting started with Caching with MediatR in ASP.NET Core
As mentioned earlier, we will first set up our required projects and library following a clean separation of concerns. I am using Visual Studio 2019 Community for this implementation. You can find the complete source code here.
Setting up the Project
So, we create a new solution with 3 projects in it.
- API - contains the controllers and service registrations
- Core - contains the entities (customer) , features(mediatr handlers) , abstractions(customer service interface) and actual MediatR Response Caching Behavior Implementation.
- Infrastructure - In this case, Infrastructure consists of the implementation of ICustomerService and provides dummy customer data to the application.
Want to learn step-by-step how to create ASP.NET Core applications with clean and scalable architecture? Check out this guide - Onion Architecture In ASP.NET Core With CQRS ā Detailed
Installing the Required Packages
Install the following nuget packages to the Core Project
Or you can install the required package whenever the code complains about a missing reference.
Adding the Customer Entity
In the core project, create a new folder entities. Here, add a new class and name it Customer.cs
Adding the ICacheableMediatrQuery interface
Now, in the Core project create a new folder named Abstractions. Here is where all the interfaces of the application will live. Here, create a new interface and name it ICacheableMediatrQuery.cs
This interface includes certain properties that come handy while writing caching logics. Remember that this interface will be inherited by all our MediatR Handlers.
- BypassCache - determines if you want to skip caching and go directly to the database/datastore.
- CacheKey - specifies a unique cache key for each similar request
- SlidingExpiration - time in hours till which the cache should be held in the memory.
Cache Setting - appSettings
To add some extra configurability, letās add a settings class for caching that we will, later on, consume via the IOptions pattern in ASP.NET Core. This allows us to configure the SlidingExpiration via appSettings.json
Create a new class in the Core project, under the Settings folder and name it, CacheSettings.cs
Now, navigate to the API project and open up the appsettings.json. Add in the following.
Remember that we have not yet registered the CacheSetting class in the ASP.NET Container. We will be doing this later in the article.
Implementing the Caching Behavior - Explained
This piece of code is going to the star of the entire implementation.
-
Line 1 - CacheBehavior class implements
IPipelineBehavior<TRequest, TResponse\>
where the request is of type ICacheableMediatrQuery. This means that caching will be applicable only to the MediatR handlers that are of type ICacheableMediatrQuery. Pretty handy. -
Line 3 to 11 - Constructor injection of Distributed caching, Logger, and Cache Settings.
-
Line 15 - If the BypassCache property is set to true, the behavior goes to the next request/response and avoids caching.
-
Line 25 - Fetches from IDistributedCache instance and checks if any data with the passed cache key exists.
-
Line 26 to 30 - if the cache exists, then returns from the cache-store and logs āFetched from cacheā
-
Line 31 to 35 0 if there is no cache found, the pipeline waits for the response from the server, sets up the cache setting from appsetting.json, and finally adds the response to the cache-store.
-
Line 18 - waits for the response from the server/database.
-
Line 19 - creates an expiration timespan in hours, either from the appsettings or the value passed from the mediatR handler. Now, this is the best part. Each mediatR handler can decide how long a cache has to be available in the cache store.
-
Line 21 - serializes the response to a byte array.
-
Line 22 - adds the serialized data against the cache key in the cache-store along with the cache options that we built.
Interesting, yeah?
Customer Service
Now, letās move to the data part of the application. In the Core project, under the abstractions folder, add a new interface and name it ICustomerService.cs
As mentioned earlier, we will be just having two methods that can return a list of customers and specific customer respectively.
Next, go to the Infrastructure Project (which is quite empty till now), and add in a new class, CustomerService.cs. Make sure you implement the ICustomerService interface in this class.
- Line 3 to 15 - A List of Sample Customers
- Line 16 to 21 - Service method to return a specific customer by Id.
- Line 19 - Since we are not connecting to an actual database, itās important to add a delay so as to mimic the response time of a Realtime database server that may contains tens of thousands of records. In this method, the control waits for 1 second. This helps us to demonstrate caching.
- Line 20 - returns the requested customer detail.
- Line 22 to 27 - Similarly, this method waits for 3 seconds given that it fetches a huge amount of data.
Building the CQRS Handlers
Now that we have everything in place, letās start adding our MediatR handlers. You will need to add them in the Core Project under the following path - Features/Customer/Queries/{name of the handler}
We will be having two handlers in consideration here.
1. GetCustomerQuery
This Query/Handler will be responsible for returning a particular customer detail for the requested customer id via the service layers. Add a new class, Features/Customer/Queries/GetCustomerQuery.cs
- Line 1 - Note that we have used the ICacheableMediatrQuery interface.
- Line 5 - Sets the CacheKeys that is logically always unique for different customer id request.
- The remainder of the code is very straightforward, getting the data from the service implementation.
2. GetCustomerListQuery
This Query/Handler will be responsible for returning all customers via the service layers. Add a new class, Features/Customer/Queries/GetCustomerListQuery.cs
Exactly similar to the previous handler, but with a different cacheKey, obviously.
Registering the Required Services
With everything done, some final touches are required to register all the services into the service container of the ASP.NET Core application. We will create a extensions class in the Core project.
Create a new folder, Extensions in the Core Project and add a new static class, ServiceCollectionExtensions.cs
Here, we register MediatR and the CachingBehavior. Note that it is important to register MediatR in every projects that contain the Handler implementations.
Finally, navigate to the API projectās Startup class/ ConfigureServices and make changes as below.
- Line 3 - Very important! You can either use Distributed Memory or In-Memory Caches. I prefer Distributed caches as they can be extended to use external infrastructure like Redis Caching down the lifetime of projects.
- Line 4 - Adds the Core Layer registration. You may need to add a reference to the Core project from the API project to resolve this.
- Line 5 - Registers Customer Service.
- Line 6 - Makes CacheSettings available in IOptions from appsetting.json
One improvement you can make is to move line 5 and 6 to the Core Layerās Service Registration Extension. Make more sense to be there. But however, letās finish up the controller.
Wiring up the Controllers
Finally, letās add a new empty API Controller in the API project under the Controllers folder and name it CustomersController.
- Line 5 to 9 - Constructor Injection for IMediator instance.
- Line 13 - sends the requested customer id and BypassCache set to false to the GetCustomerQuery / Handler.
- Line 19 - similarly, sends a request to the mediatR pipeline.
Testing with Swagger
That should be everything needed for our implementation. Letās do some quick tests with Swagger to validate our implementation.
Quick Hack: As response time is an important parameter to our test, you can easily enable the Response Duration in Swagger UI by the following code change in the API/Startup/Configure method.
Build and run the application.
Letās test the GetById endpoint.
You can note the first request took about 1.2 seconds. Theoretically, as soon as this response comes back to Swagger UI, in the background, our ASP.NET Core applicationsās MediatR pipeline would have cached this request. Letās request for the same customer id.
There you go, just 55 milli-seconds. Thatās quite a noticeable speed improvement, yeah? You would be getting similar results while connecting to a database with loads of data as well.
Other Improvements include:
- You can allow the client/API consumer to decide whether they need cached data or not, by exposing the BypassCache parameter to the API endpoint. This would be more practical.
- Cache invalidation should happen whenever there is Write operation to the associated data store. This avoids the data inconsistency issue with caching.
Thatās all for now. Letās wrap up the article ;)
Summary
In this article, we have learned an awesome way to implement Caching with MediatR in ASP.NET Core application in a powerful yet simple way. We took advantage of MediatRās Pipeline behavior to implement this. There are many more use-cases for the MediatR pipeline. Whatās your favorite? You can also find theĀ complete source code on my Github here. Have any suggestions or questions? Feel free to leave them in the comments section below. Thanks and Happy Coding! š