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

Service Endpoints --

 1. Create a VNET with 2 subnets 2. Create a service endpoint policy. That enables us to specify which particular storage account to connect to. This is currently enabled for storage accounts alone. Otherwise as part of service endpoints we can specify the resource provider alone and not a specific resource. We can specify this service endpoint policy while creating a service endpoint. Only storage account allow service endpoint policies as of now. If we notice then selecting cosmos db or any other service other than storage will not have the ability to accept service endpoint policies. As mentioned microsoft.storage allows us to add service endpoint policies. Thus we can specify a particular resource within a provider This is 1 way of enabling service endpoints. Or we can directly go to the storage account and from the networking tab select Sub1. This will automatically configure service endpoints and will add a service endpoint policy that enables the resource to connect to the s...

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

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