Skip to main content

Refresh Cache using Redis and BackgroundService .net 5

Install Nuget package  Microsoft.Extensions.Caching.StackExchangeRedis

Creating a cache attribute

[Cached(time: 10)]

 public async Task<IActionResult> GetAll() => Ok(await SalesdbContext.Employees.ToListAsync());

We create an attribute called cached that accepts the expiry time for the cached data. Once the expiry time is over the response returned will be null and we will fetch the data from the database and update the cahce.

[AttributeUsage(AttributeTargets.Method)]
    public class CachedAttribute : Attribute, IAsyncActionFilter
    {
        public int Time { get; set; }

        public CachedAttribute(int time)
        {
            Time = time;
        }

        public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {

            //Before Controller is invoked

            var cacheSettings = context.HttpContext.RequestServices.GetRequiredService<CacheSettings>();

            //if cache is not enabled
            if (cacheSettings == null || !cacheSettings.Enabled) await next();

            var responseCacheService = context.HttpContext.RequestServices.GetRequiredService<IResponseCacheService>();

            //Get a key to for the cache entry.. generate from current url and headers
            // bcoz versioning headers can change and the cache needs to 
            // return based on headers also
            string cacheKey = GetCacheKeyFromRequest(context.HttpContext.Request);

            //Get the value for the cache
            // We have set a cache period of 10 secs and after 10 secs
            // the data becomes stale and is not returned from the API.
            var data = await responseCacheService.GetCachedData(cacheKey);

            if (!string.IsNullOrWhiteSpace(data))
            {
                context.Result = new ContentResult()
                {
                    Content = data,
                    StatusCode = 200,
                    ContentType = "application/json"
                };
                return;
            }

            //After Controller code is executed
            var response = await next();

            // Get the result from the response and cache it 
            if (response.Result is OkObjectResult okObjectResult)
            {
                await responseCacheService.CacheReponse(cacheKey, okObjectResult.Value, TimeSpan.FromSeconds(Time));
            }


            string GetCacheKeyFromRequest(HttpRequest request)
            {
                var cacheKey = new StringBuilder();

                cacheKey.Append($"{request.Path}");

                foreach (var item in request.Query.OrderBy(i => i.Key))
                {
                    cacheKey.Append($"|{item.Key}_{item.Value}");
                }

                return cacheKey.ToString();
            }
        }



    }


The attribute behaves similar to a middleware, where we have the request delegate. The next() calls the cache to invoke the action. We check if the data exists in the cache , if yes then we generate a file name using the url and the request headers. We use request headers so that if the API implements versioning then we cache the data based on the versions otherwise we might return incorrect results for the version.

If the cache is empty or the settings for cache is disabled then we call the request delegate and get the response back from the action and then add it to the cache.


 "CacheSettings": {
    "Enabled": true,
    "ConnectionString": "localhost:6379"
  }


BACKGROUND SERVICE

The background service will be used to cache the request before the cache expires, so that we dont have to get the data from the database. This service updates the cache every 5 secs, this is done bcoz the cache expires in 10 secs. 


using AspNetCore.HostedServices.Core.Services;
using AspNetCore.HostedServices.EF;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace AspNetCore.HostedServices.BackgroundServices
{
    public class StudentsCacheService : BackgroundService
    {
        int intervalToCacheData = 5;
        string key = "/students";
        public StudentsCacheService(IResponseCacheService responseCacheService, IConfiguration configuration, IServiceScopeFactory scopeFactory)
        {
            ResponseCacheService = responseCacheService;
            ScopeFactory = scopeFactory;
        }

        public IResponseCacheService ResponseCacheService { get; }
        public IServiceScopeFactory ScopeFactory { get; }
        public salesdbContext SalesdbContext { get; }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                var cachedData = await ResponseCacheService.GetCachedData(key);

                if (string.IsNullOrWhiteSpace(cachedData))
                {
                    //since background services are singleton and 
                    //DBcontext is scoped we cannot directly inject dbcontext
                    using (var scope = ScopeFactory.CreateScope())
                    {
                        var dbContext = scope.ServiceProvider.GetRequiredService<salesdbContext>();
                        var dataFromDb = await dbContext.Employees.ToListAsync();
                        await ResponseCacheService.CacheReponse(key, dataFromDb, TimeSpan.FromSeconds(10));
                    }
                }

                await Task.Delay(TimeSpan.FromSeconds(intervalToCacheData));
            }
        }
    }
}


CACHE SERVICE

using AspNetCore.HostedServices.Core.Services;
using Microsoft.Extensions.Caching.Distributed;
using Newtonsoft.Json;
using System;
using System.Threading.Tasks;

namespace AspNetCore.HostedServices.Infrastructure.Services
{
    public class ResponseCacheService : IResponseCacheService
    {
        public ResponseCacheService(IDistributedCache distributedCache) => (DistributedCache) = (distributedCache);

        public IDistributedCache DistributedCache { get; }

        public async Task CacheReponse(string key, object value, TimeSpan time)
        {
            if (string.IsNullOrWhiteSpace(key)) throw new Exception("Key cannot be null");

            await DistributedCache.SetStringAsync(key, JsonConvert.SerializeObject(value), new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = time
            });
        }

        public async Task<string> GetCachedData(string Key) => await DistributedCache.GetStringAsync(Key);
    }
}


APPSETTINGS.JSON


   var cacheSettings = new CacheSettings();
            Configuration.GetSection(nameof(CacheSettings)).Bind(cacheSettings);
            services.AddSingleton(cacheSettings);
            
            services.AddStackExchangeRedisCache(op => op.Configuration = cacheSettings.ConnectionString);

            services.AddDbContext<salesdbContext>();

            services.AddHostedService<StudentsCacheService>();

            services.AddSingleton<IResponseCacheService, ResponseCacheService>();






































































































































































Comments

Popular posts from this blog

Az-500 NSG and ASG

Application security groups Application security groups enable you to configure network security as a natural extension of an application's structure, allowing you to group virtual machines and define network security policies based on those groups. You can reuse your security policy at scale without manual maintenance of explicit IP addresses. The platform handles the complexity of explicit IP addresses and multiple rule sets, allowing you to focus on your business logic. To better understand application security groups, consider the following example: In the previous picture,  NIC1  and  NIC2  are members of the  AsgWeb  application security group.  NIC3  is a member of the  AsgLogic  application security group.  NIC4  is a member of the  AsgDb  application security group. Though each network interface in this example is a member of only one network security group, a network interface can be a member of multiple app...

Azure AD Authentication And Authorization

ROLE BASED AUTHORIZATION Step1:   Setup  API. 1. Create an app registration and create an app role. Make sure that the app role is assigned to user and applications. We add it to both user groups and applications as this role can be then assigned to both users and applications. Scopes can only be assigned to apps. Now we can have only users with this role access our application. This app role behind the scenes adds an approles section to the manifest.json. We can directly add it to the manifest file also. Step 2:  Setup an app registration for the UI/ WEB App. . We will grant this app the read role created in the API app (shown above). Go to Azure AD and select the UI app registration. When we register an application 2 elements get created. 1. App registration  2. Enterprise Application -- service principal that get created for the app Adding roles to applications Go to the App registration => API Persmissions => Add a Permission => My API's The My Api's sec...

ASp.net core 3.1 identity

It is just an extension to cookie authentication. We get a UI, Tables, helper classes, two factor authentication etc. Even EF and its database constructs. So instead of writing code for all of this we can just use these in built features. Extending Default Identity Classes Add a class that inherits from    public class AppUser : IdentityUser     {         public string Behavior { get; set; }     } Also change the user type in login partial.cs under shared folder Then add migrations and update db using migrations. We can customize further.  services.AddDefaultIdentity<AppUser>(options =>              {                 options.SignIn.RequireConfirmedAccount = true;                 options.Password.RequireDigit = false;           ...