skip to Main Content

Implementing Request Caching in ASP.Net Core

At some point in an application’s development, usually, fairly early on, you realize the application is slow. After some research, the culprit is, unnecessarily retrieving the same data, and a light goes off, and you think: “I need some caching.”

Caching is an invaluable pattern for eliminating redundant calls to a database or a third party API. Microsoft provides IMemoryCache for time-based caching, however sometimes time-based caching isn’t what you need. In this article, we look at Request Scoped caching and how it can benefit us.

What is Request caching? Request caching is a mechanism to cache data for the life of a web request. In dot-net, we’ve had this ability in some capacity with the HttpContext.Items collection, however, HttpContext is not known for its injectability.

Request Scoped caching has a few benefits: First, it eliminates the concern of stale data. In most scenarios, a request executes in less than a second and which typically isn’t long enough for data to become stale. And secondly, expiration isn’t a concern because the data dies when the request ends.

Out of the box, Asp.Net Core doesn’t have injectable caching. As mentioned earlier, HttpContext.Items is an option, but it’s not an elegant solution.

Luckily for us, ASP.Net Core gives us the tools to create an injectable Request Caching implementation by using the built-in dependency injection (DI) framework.

The built-in DI framework has three lifetimes for dependencies: Singleton, Scoped, and Transient. Singleton is for the life of the application, Scoped is for the life of the request and Transient is a new instance with each request.

I’ve created an interface modeled after the IMemoryCache interface to keep things consistent.

Interface

public interface IRequestCache
{
    /// <summary>
    /// Add the value into request cache. If the key already exists, the value is overwritten.
    /// </summary>
    /// <param name="key"></param>
    /// <param name="value"></param>
    /// <typeparam name="TValue"></typeparam>
    void Add<TValue>(string key, TValue value);

    /// <summary>
    /// Remove the key from the request cache
    /// </summary>
    /// <param name="key"></param>
    void Remove(string key);

    /// <summary>
    /// Retrieve the value by key, if the key is not in the cache then the add func is called
    /// adding the value to cache and returning the added value.
    /// </summary>
    /// <param name="key"></param>
    /// <param name="add"></param>
    /// <typeparam name="TValue"></typeparam>
    /// <returns></returns>
    TValue RetrieveOrAdd<TValue>(string key, Func<TValue> add);

    /// <summary>
    /// Retrieves the value by key. When the key does not exist the default value for the type is returned.
    /// </summary>
    /// <param name="key"></param>
    /// <typeparam name="TValue"></typeparam>
    /// <returns></returns>
    TValue Retrieve<TValue>(string key);
}

Implementation

public class RequestCache : IRequestCache
{
    IDictionary<string, object> _cache = new Dictionary<string, object>();

    /// <summary>
    /// Add the value into request cache. If the key already exists, the value is overwritten.
    /// </summary>
    /// <param name="key"></param>
    /// <param name="value"></param>
    /// <typeparam name="TValue"></typeparam>
    public void Add<TValue>(string key, TValue value)
    {
        _cache[key] = value;
    }

    /// <summary>
    /// Remove the key from the request cache
    /// </summary>
    /// <param name="key"></param>
    public void Remove(string key)
    {
        if (_cache.ContainsKey(key))
        {
            _cache.Remove(key);
        }
    }

    /// <summary>
    /// Retrieve the value by key, if the key is not in the cache then the add func is called
    /// adding the value to cache and returning the added value.
    /// </summary>
    /// <param name="key"></param>
    /// <param name="add"></param>
    /// <typeparam name="TValue"></typeparam>
    /// <returns></returns>
    public TValue RetrieveOrAdd<TValue>(string key, Func<TValue> add)
    {
        if (_cache.ContainsKey(key))
        {
            return (TValue)_cache[key];
        }

        var value = add();

        _cache[key] = value;

        return value;
    }

    /// <summary>
    /// Retrieves the value by key. When the key does not exist the default value for the type is returned.
    /// </summary>
    /// <param name="key"></param>
    /// <typeparam name="TValue"></typeparam>
    /// <returns></returns>
    public TValue Retrieve<TValue>(string key)
    {
        if (_cache.ContainsKey(key))
        {
            return (TValue)_cache[key];
        }

        return default(TValue);
    }
}

Using ASP.Net Core’s DI framework we’ll wire it up as Scoped.

services.AddScoped<IRequestCache, RequestCache>();

Usage

public class UserService
{
    private readonly IRequestCache _cache;
    private readonly IUserRepository _userRepository;

    public UserService(IRequestCache cache, IUserRepository userRepository)
    {
        _cache = cache;
        _userRepository = userRepository;
    }

    public User RetrieveUserById(int userId)
    {
        var buildCacheKey = UserService.BuildCacheKey(userId);

        return _cache.RetrieveOrAdd(BuildCacheKey, () => { return _userRepository.RetrieveUserBy(userId); });
    }

    public void Delete(int userId)
    {
        var buildCacheKey = UserService.BuildCacheKey(userId);

        _userRepository.Delete(userId);
        _cache.Remove(BuildCacheKey(userId));
    }

    private static string BuildCacheKey(int userId)
    {
        return $"user_{userId}";
    }
}

That’s it! Request Caching is now injectable in any place you need it.

Visit the Git Repository and feel free to take the code for a spin.

Running Await in a Constructor

If you must run code in a constructor. I’d look for a different way, but if you must, here’s one way:

public class MyClass
{
    public MyClass()
    {
        Task.Run(async () => {
            var result = await AsyncActivity();
        });
    }
}

Workaround for ‘Template parse errors’ in Angular

This was one of the more frustrating issues with Angular 2/4/+. It’s not an issue with Angular 2/4/+ per se, but with how webpack bundles the supporting HTML files.

This issue happens when you run webpack with the production flag (webpack -p). The compilation completes, but running the site generates a runtime error: Template parse errors

Uncaught Error: Template parse errors:
Unexpected closing tag "a". It may happen when the tag has already been closed by another tag. For more info see https://www.w3.org/TR/html5/syntax.html#closing-elements-that-have-implied-end-tags ("<i class="fa fa-home"></i> Home
 	<li class="nav-item"><a class="nav-link" href="/ams/#/search">AMS[ERROR ->]</a></li>
 	<li class="nav-item"><a class="nav-link" href="/ams/#/logout"><i class="fa fa-adn"></i> Logout</a"): ng:///e/e.html@0:557 Unexpected closing tag "a". It may happen when the tag has already been closed by another tag. For more info see https://www.w3.org/TR/html5/syntax.html#closing-elements-that-have-implied-end-tags ("MS</a></li>
 	<li class="nav-item"><a class="nav-link" href="/ams/#/logout"><i class="fa fa-adn"></i> Logout[ERROR ->]</a></li>

The suspected HTML:

<div class="navbar navbar-light bg-faded rounded navbar-toggleable-md">
<div class="collapse navbar-collapse" id="containernavbar">
        <global-search></global-search>
<ul class="navbar-nav">
 	<li class="nav-item active"><a class="nav-link" routerlink="['']"><i class="fa fa-home"></i> Home</a></li>
 	<li class="nav-item"><a class="nav-link" href="/ams/#/search">AMS</a></li>
 	<li class="nav-item"><a class="nav-link" href="/ams/#/logout"><i class="fa fa-adn"></i> Logout</a></li>
</ul>
</div>
</div>

I couldn’t find anything wrong with this HTML. I ran it through multiple online HTML validators, and the HTML always came back as valid HTML. After a few weeks of beating my head against the wall and getting nowhere, I figured there must be a way around this issue.

What I discovered, via a GitHub thread (sorry I lost the link to it) was it’s the HTML loader trying to minimize the HTML that’s causing the problem. If the HTML minimization is turned off, this issue goes away. To be fair, most folks with this error had invalid HTML. I encourage you to check your HTML before working around this issue, you may be just pushing the issue down the road.

Turning off HTML minimization is easy as updating your HTML-loader in the webpack.config.js.

Before:

{
test: /.html$/,
loader: 'html-loader'
},

After:

{
test: /.html$/,
use: [{
loader: 'html-loader',
options: {
minimize: false,
removeComments: false,
collapseWhitespace: false,
removeAttributeQuotes: false,
}
}],
},

In a Single Page Application, Should I process on the Client or the Server?

One of the selling points of the Single Page Application (SPA) was offloading work traditionally performed on the server onto the client. I feel the SPA has delivered on this promise.

However, it’s not all roses. It’s easy to get overzealous and push too much work to the client. We often forget that we can’t control the client’s environment — the client can be anything from a ten-year-old machine to the latest-and-greatest smartphone with access to varying internet speeds. The only thing we can count on is the user viewing our site in a browser.

Processing

In my experience, intensive processing and data needing consistency between the server and the client, such as date conversions or data requiring precise calculations, such as money, are prime candidates for server-side rendering.

Paging

A common task performed on the client is paging. With small datasets this works great; however, small datasets never stay small. As data grows, the application slows and eventually becomes unresponsive. Unfortunately, you don’t know it’s happening because it’s on the client and even worse it doesn’t occur on all clients making it hard to troubleshoot.

Moving paging from the client to the server will alleviate paging related performance issues on the client. You might be thinking, “but now I am making a ton of API calls. Making so many API calls doesn’t seem optimal.” True, it does seem that way, but you’ll be surprised how fast your data returns from the server. And the best part? You control the server and can increase capacity as needed.

At the end of the day, you want to provide the user a rich responsive experience and sometimes that’s letting the server do the heavy lifting.

Summary

Summarizing, when possible we want to offload work to the client, but by doing so, we can quickly overburden the client. Keeping arduous tasks such as paging and intensive processing on the server can protect us from overwhelming the client.

Back To Top