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

App Role assignment to service principal --

 Using Ms Graph Rest API's Permissions One of the following permissions is required to call this API. To learn more, including how to choose permissions, see  Permissions . Permission type Permissions (from least to most privileged) Delegated (work or school account) AppRoleAssignment.ReadWrite.All and Application.Read.All, AppRoleAssignment.ReadWrite.All and Directory.Read.All, Application.ReadWrite.All, Directory.ReadWrite.All Delegated (personal Microsoft account) Not supported. Application AppRoleAssignment.ReadWrite.All and Application.Read.All, AppRoleAssignment.ReadWrite.All and Directory.Read.All, Application.ReadWrite.All, Directory.ReadWrite.All Create 2 app registrations. App role owner will contain the app role that will be assigned to a service principal. The  reader role in approleowner will be added to the approlesubscriber Setup postman to use the Oauth auth flow to get a token for MS Graph. ClientId:   Application (client) ID for approlesubscrib...

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;           ...

Get user groups

 string[] scopes = new string[] { "https://graph.microsoft.com/.default" };             string clientId = "";             string tenantId = "";             string secret = "";                        var options = new TokenCredentialOptions             {                 AuthorityHost = AzureAuthorityHosts.AzurePublicCloud             };             // https://learn.microsoft.com/dotnet/api/azure.identity.clientsecretcredential             try             {                 var clientSecretCredential = new ClientSecretCredential(                        ...