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

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

Function APP and KV integration

 Create a function App and enable system assigned identity Create  a Keyvault and add a secret (Name in my case) Configure Access policies for the function app in keyvault Create an  access policy in Key Vault   for the application identity you created earlier. Enable the "Get" secret permission on this policy. Do not configure the "authorized application" or   applicationId   settings, as this is not compatible with a managed identity. https://docs.microsoft.com/en-us/azure/app-service/app-service-key-vault-references Key Vault references currently only support system-assigned managed identities. User-assigned identities cannot be used. We are granting our function app permissions to get and list all the secrets in this keyvault. Add Key Vault secrets reference in the Function App configuration Go to the keyvault and click on the secret we just created. Copy the secret identifier. We need to add this value to the function app configuration.  @Microsof...

AZ-500 AppService Inside VNet

The intent is to create an appservice that is available inside a VNET. So we will create a vnet and put an appservice inside it. This Vnet will have 2 subnets. One that has the appservice and another one that will have a VM.. We need to make sure that this Appservice can be accessed only inside the VM (inside the same VNET)  1. Create a Vnet Total Address space is 256.. We split into 2 subnets 2. Create an Appservice in standard or above tier as the lower tiers dont support networking.  and select the Vnet and select the sub2 subnet 3. Create a Virtual Machine inside sub1. 4. Go to Appservices and onto the networking tab and select Access Restrictions Create a rule and select VNET and subnet that we created earlier. We can also specify IP Address. Then this appservice can be accessed by the specified IP's only. So this appservice can only be accessed within subnet 1. Which is where we have deployed the VM.. From Internet Inside VM (Subnet 1)