Adding Response Caching to your .NET Core Web APIs Quickly

Dale Bingham
8 min readApr 28, 2020

--

When thinking performance, there are some non-complex ways to quickly add great performance to .NET Core Web APIs. I thought I was going to need Redis and another container, networking, complex logic, and a lot of trial and error to get caching APIs to work correctly. Turns out after studying the different ways for a couple hours, it took about 10 minutes of coding to add great performance to my APIs for what I needed! Below is how I did it and some links for safe keeping. You may be able to do the same.

The Idea

Putting it in basic terms, I wanted to have my application APIs that deliver JSON content to perform faster. For a single user on a local computer serving local content it won’t make too huge of a deal. However, for a busy API serving local, remote, and mobile clients it may help quite a bit. And I wanted to see if I could add caching without all the hoopla of Memcached or Redis, a lot of configuration, docker or docker-compose and networks, and all that.

Turns out after I researched and read up a bit, it was pretty easy to do. Granted this is an easy case for me for my OpenRMF tool. I wanted to cache lists of values and data used and reused, and it did not matter what user requested it. That is of course a fairly simple use case. It turned out to only be a few lines of code to get this rolling. See below. And feel free check out the GitHub repo I made to show the differences of caching and not caching the content.

My setup: I have a brand new MacBook 16" screen with a lot of updates. That is what I am running this on. That said, the idea and model of caching does not change in how I did this, regardless of the type of machine. (I had my old one for 8+ years and was maxing it out with power and memory I needed to run all I am doing. So it was time to update and give my older one to my 10 yr old daughter to use for school.)

Options on Caching

So let’s jump in. There are a few options on caching, and I put links at the bottom of this article for you to start your journey learning or refreshing your memory on them. You have the In-memory caching which is where you can store items in memory (duh!) of your web server / container running your API. You have the option of ResponseCache which is cache stored on the server and/or client browser calling your API. And you have distributed cache which is an external service to the web applications that serves the cache.

You should research the types below to determine which you will need. I chose ResponseCache as it is all I needed, was easy to implement, and did not require much time for the payback received. I will caveat this by saying I only have 1 instance of each API, no sticky-session type of setup, and I am running my APIs either locally or on a single-pod setup in a container in Kubernetes. So I can do this type of caching fairly easily. You may be able to as well.

You will have to determine how you run your APIs, your services, and your web clients and test to see which gives you the most benefit while maintaining security, simplicity, and performance.

Enough words — let’s see the impact!

Example Without Caching

The first way I ran this is to run dotnet new webapi -n non-caching using .NET Core 2.2 to setup a Web API project. (You can use 3 or 3.1 as well. I just had code in .NET Core 2.2 I used this for. The upgrade to 3.1 for my app is this summer 2020.) I added an in-memory database and loaded up the NIST 800–53 controls XML file from my OpenRMF application into that one table in memory that I can query. See the GitHub repo for more information.

I have a ControlsController.cs file in the Web API setup that serves up 2 endpoints that reference this data. The regular “/” endpoint and then a “/majorcontrols” endpoint. Basically, I am sending back a list of NIST controls in the / call and just the major family listing in the 2nd. The API calls and what they are for are not the main point though. This model can be used for any of your GET based APIs that you think can use some caching techniques and are viable options. More on viability and fit below.

As you can see in the below image when I call the / API endpoint with a regular GET and no parameter options, it takes on average 21ms to “call” and then almost 34ms to “get” the content. This is a rough average. I ran this 10 times to be sure the calls were all about the same. Realistically, 57ms is NOT a long time to humans. I will give you that! However, in computer time that is a lot.

There is HTML code in the GitHub repo that can show you how to call this with a web page correctly. See the image just below for the performance information.

Performance time on the same code without caching enabled.

Example With Caching

Now enter the caching of this API. There are a few things you have to do in order to enable caching. In my GitHub repo I used the same exact code for the non-caching and copied to a caching folder. Then in the /caching folder I included these few lines in the Startup.cs and Controller files to enable caching.

There are three steps to this outlined below. Again feel free to go to the GitHub repo to see the actual code setup.

Step 1: In the ConfigureServices(IServiceCollection services) method of Startup.cs near the bottom just above “.AddMvc()” I added the line below:

services.AddResponseCaching();

Step 2: Below that in Startup.cs in the Configure(IApplicationBuilder app, IHostingEnvironment env) method I added the line below just before .UseMvc():

app.UseResponseCaching();

Step 3: Finally in the actual Controller file I added a directive just above the API calling code that said to use ResponseCaching like below:

[ResponseCache(Duration = 60, Location = ResponseCacheLocation.Any, VaryByQueryKeys = new [] {"impactlevel", "pii"})]

And that is it!

If ResponseCache is good for you to use, you need to understand how you would use it. In one of my examples in GitHub I had to use the VaryByQueryKeys so I cache the data correctly based on the 2 parameters used for that API. In another API call, it always returns the same data with no given parameters, so the VaryByQueryKeys was not required. Your specific implementation will differ based on your API structure.

As you can see in the below image when I call the / API endpoint with a regular GET and no parameter options, it takes on average .38ms or 380 nanoseconds to “call” and then almost 4ms to “get” the content. This is again an average. I ran this 10 times to be sure the calls were all about the same not counting the first one that I ran to actually CACHE the information. Caching in my setup was set to last 60 seconds.

Again, we are talking milliseconds and it is not a big difference to a human. However, in computer time that is a lot. To go from 21ms to .38ms is more than an order of magnitude. And a fast response time to a user that is using your web application or mobile application, calling a lot of APIs and data, is a big deal!

Performance time on the same code with caching enabled. After the 1st call, cached content used.

It shocked me that just a few lines of code was added and I was done! I then started to think of where else in my application I could use caching without defeating the purpose of the API, ruining security, and keeping data fresh correctly. That is an exercise I leave up to you.

A Few Things to Keep in Mind

I did have some things to think about in my OpenRMF application. In that setup, I have a few APIs and several calls per API. And I had to ask myself… could I cache my data and have it still be relevant? If someone updates data and it does not show the changes for 30 or 60 seconds, is that harmful or showing incorrect results? Is there security around the data returned that I need to not cache and go get every single time based on the user and their roles in requesting it? Are there policies, rules, use cases, or even laws around caching my specific content?

Other things concerning the infrastructure also come into play here. Will I be running this in an application container and will there be more than 1 replica? Do I need to survive restarts of my application and keep caching? There are probably more, I just wanted to get your brain thinking in those terms as well when you go down this road.

I ran through all my endpoints and for now, I have 7 or 8 that were drastically improved in performance by adding caching. The change in results were very similar to the examples above. And it did not hurt the data security, the truth of the information, or the user running the application. Again, your results and implementations may vary.

Useful Links

This is just to get you introduced to response caching in .NET Core and show you how easy it was to do. I hope that helped you some! It did help me. And since I was so shocked at how easy this was for .NET Core Web APIs I was thinking others may want to learn this quickly as well.

I am also getting older, so putting it here helps me when I forget what I did! No doubt.

Happy Coding!

--

--

Dale Bingham

CEO of Soteria Software. Developer on OpenRMF. Software Geek by trade. Father of three daughters. Husband. Love new tech where it fits. Follow at @soteriasoft