icons

Custom response caching in ASP.NET Core

iFour Team - February 11, 2021

Listening is fun too.

Straighten your back and cherish with coffee - PLAY !

  •  
  •  
  •  
Custom response caching in ASP.NET Core
Table of Content

Cache invalidation is the process that entries in the cache which can be replaced or removed. The server cannot force the new version of the page to be used instead of the cached page, once the page is cached in the browser. So, pages likely to change cannot be cached for a very long time at the client and we cannot change this too.

On the server-side, built-in response caching does not permit cache invalidation. Same cache duration for both for instructing the browser to cache the page, and for server-side caching. In some applications, these restrictions make the built-in response caching unusable. We will execute the cache for a single web server using IMemoryCache.

There are certain three methods to invalidate the cache but not all caching proxies support these methods.

Purge: It will remove the content from proxy caching immediately. If the client requests the data again, then data will be fetched from the application and stored in cache proxy. All variants of the cached content will be removed by this method.

Refresh: Even if the cached content is available, it will fetch the data from the application. The new version of data will replace the previously stored content in the cache form application. Only one variant of cached content will be affected by this method.

Ban: As the name suggested, cached content will be added to the blacklist. The client can request the blacklist, if the content matches then new content is fetched from the app and added to the cache. This method does not immediately remove cached content from the proxy cache. Instead, cached content is updated with the new client request that information.

In our blog, to allow caching to be fast we will add the middleware.

IMemoryCache

IMemoryCache is similar to the System.Runtime.Caching.MemoryCache.

    public interface IMemoryCache :IDisposable
    {
    bool GetValue(object key, out object value);
    ICacheEntryCreateEntryValue(object key);
    void RemoveCache(object key);
    }

In MVC, it will be automatically registered. We can register memory cache in configure services method using:

services.AddMemoryCache();

IMemoryCache Example

If we have data in the cache memory then we will use the IMemoryCache to avoid getting data from the database query.

    public class CustomerService
    {
    private const string CustomerCacheKey = "customer-cache-key";

    private readonlyIMemoryCache _cache;
    private readonlyIDatabase _db;
    public CutomerService(IMemoryCache cache, IDatabasedb)
    {
    _cache = cache;
    _db = db;
    }

    public async Task>GetCustomers()
    {
        if (_cache.TryGet(CustomerCacheKey , out IEnumerable customers))
        {
            return customers;
        }

        customers= await _db.getAll(...);

        _cache.Set(CustomerCacheKey , customers, ...);

        return customers;
    }
}

When we are saving data in cache then the "Set" method provides many options to expire cache.

_cache.Set("key", item, TimeSpan.FromDays(1));

Example

Control Server Caching

To control the cached individual page, we will use the action filter attribute.

    public class CacheActionAttribute :ActionFilterAttribute
    {
    public int? ClientCacheDuration{ get; set; }
    public int? ServerCacheDuration{ get; set; }
    public string Tags { get; set; }

    public override void OnActionExecuting(ActionExecutingContext context)
    {
    if (ClientCacheDuration .HasValue)
    {
    context.HttpContext.Items[Constants.ClientCacheDuration ] = ClientCacheDuration .Value;
    }

    if (ServerCacheDuration .HasValue)
    {
    context.HttpContext.Items[Constants.ServerCacheDuration ] = ServerCacheDuration .Value;
    }

    if (!string.IsNullOrWhiteSpace(Tags))
    {
    context.HttpContext.Items[Constants.Tags] = Tags;
    }

    base.OnActionExecuting(context);
    }
    }

This will add attribute to the HttpContext items collection. This method is used for communication between caching middleware and the action.

Using Middleware caching

    public async Task Invoke(HttpContext context)
    {
    varcacheKey = CacheKeyBuild(context);

    if (_cache.TryGet(cacheKey , out CachedPage page))
    {
    await WriteResponse(context, page);

    return;
    }

    ApplyClientHeaders(context);

    if (IsNotServerCachable(context))
    {
    await _next.Invoke(context);

    return;
    }

    page = await CaptureResponse(context);

    if (page != null)
    {
    varcacheServerDuration = GetCacheDuration(context, Constants.ServerDuration);

    if (cacheServerDuration.HasValue)
    {
    var tags = GetCacheTags(context, Constants.Tags);

    _cache.Set(cacheKey , page, cacheServerDuration.Value, tags);
    }
    }
    }

We can build cache key as per our requirement. If the page is retrieved from the cache then it is written to the response.

If the page is not found then we have to do some code. If applicable, first we set the client caching header and then check if we can cache the request. Here, we will only want to GET methods if another method is used then we should invoke the middleware component.

    private async Task WriteResponse(HttpContext context, CachedPage page)
    {
    foreach (varcacheHeader in page.Headers)
    {
    context.Response.Headers.Add(cacheHeader );
    }

    await context.Response.Body.WriteAsync(page.Content, 0, page.Content.Length);
    }

Now we have to capture the requested output from the middleware component.

Capture the response

Capture the page response to swap default response body stream with MemoryStream.

    private async Task
        CaptureResponsePage(HttpContext context)
        {
        varresponsePageStream = context.Response.Body;

        using (varbufferMemory = new MemoryStream())
        {
        try
        {
        context.Response.Body = bufferMemory ;

        await _next.Invoke(context);
        }
        finally
        {
        context.Response.Body = responsePageStream ;
        }

        if (bufferMemory .Length == 0) return null;

        var bytes = bufferMemory .ToArray();

        responsePageStream .Write(bytes, 0, bytes.Length);

        if (context.Response.StatusCode != 200) return null;

        return BuildCachedPage(context, bytes);
        }
        }
    

Searching for Dedicated ASP.Net Core Web Developer ? Your Search ends here.

Client Caching

When we have to send cache header to the browser, we will use the simple approach.\

    public void SendClientHeaders(HttpContext context)
    {
    context.Response.OnStarting(() =>
    {
    varcacheDurationClient = GetCacheDuration(context, Constants.ClientCacheDuration );

    if (cacheDurationClient .HasValue&&context.Response.StatusCode == 200)
    {
    if (cacheDurationClient == TimeSpan.Zero)
    {
    context.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue
    {
    NoCache = true,
    NoStore = true,
    MustRevalidate = true
    };
    context.Response.Headers["Expires"] = "0";
    }
    else
    {
    context.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue
    {
    Public = true,
    MaxAge = cacheDurationClient
    };
    }
    }

    return Task.CompletedTask;
    });
    }

ClientCacheDuration is coming from the action filter. Now we will invalidate the cache.

Cache Invalidation

We will remove all the cache by custom approach before they naturally expire.

    public void RemoveCache(string cacheKey)
    {
    _cache.Remove(Constants.CacheKeyPrefix + cacheKey);
    }

    public void RemoveByTag(string tag)
    {
    if (_cache.TryGetValue(Constants.CacheTagPrefix + tag, out CancellationTokenSourcetokenSource))
    {
    tokenSource.Cancel();

    _cache.Remove(Constants.CacheTagPrefix + tag);
    }
    }

    public void RemoveAll()
    {
    RemoveByTag(Constants.AllTag);
    }

As we can see here, removing a single cache entry is simple by sending a cache key. RemoveAll and RemoveByTag method requires the CancellationTokenSource from the cache, and call the cancel method. Cancel method expires all token that is issued by the source. As a result, it will remove cache entries.

Conclusion

In this blog, we have done the caching using the IMemoryCache instead of IDistributedCache. Because IDistributedCache reduces the functionality and it does not support the token expiration. We have seen how to use middleware for cache response. To save the page in the memory cache, we have used the action filter.

Technology Stacks

Technology that meets your business requirements

Planning a cutting-edge technology software solution? Our team can assist you to handle any technology challenge. Our custom software development team has great expertise in most emerging technologies. At iFour, our major concern is towards driving acute flexibility in your custom software development. For the entire software development life-cycle, we implement any type of workflow requested by the client. We also provide a set of distinct flexible engagement models for our clients to select the most optimal solution for their business. We assist our customers to get the required edge that is needed to take their present business to next level by delivering various out-reaching technologies.

Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo
Logo

Our Global Presence

Get advanced technology that will help you find the
right answers for your every business need.

Get in touch

Need a Consultation?

Drop us a line! We are here to answer your questions 24/7.