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
Post a Comment