ASP.NET Core uses filter and redis for interface overload prevention

Background

In daily development, it is often necessary to add anti-weight functions to some key business interfaces whose response is not very fast, that is, multiple identical requests received in a short period of time, only one is processed, and the rest is not processed, so as to avoid dirty data. This is slightly different from idempotency, which requires the same effect and result as repeated requests, and usually requires checking the state before performing business operations inside the interface; Anti-duplication can be considered a business-independent general function, and we can implement it with the help of Filter and Redis in ASP.NET Core.

About Filter

The origins of Filter can be traced back to ActionFilter in ASP.NET MVC and ActionFilterAttribute in ASP.NET Web API. ASP.NET Core unifies these different types of filters into a single type, called Filter, to simplify the API and increase flexibility. ASP.NET Core, Filter can be used to implement various functions such as authentication, logging, exception handling, performance monitoring, and so on.


image


By using Filter, we can run custom code before or after a specific stage of the request processing pipeline to achieve AOP effect.


image


Coding implementation

The idea of the anti-heavy component is very simple, store some parameters of the first request as identifiers in redis, set the expiration time, and the next time the request comes, first check whether the same request for redis has been processed;
As a generic component, we need to be able to customize the fields used as identifiers and expiration times, and we will start implementing them.

PreventDuplicateRequestsActionFilter

public class PreventDuplicateRequestsActionFilter : IAsyncActionFilter
{
public string[] FactorNames { get; set; }
public TimeSpan? AbsoluteExpirationRelativeToNow { get; set; }
private readonly IDistributedCache _cache;
private readonly ILogger _logger;
public PreventDuplicateRequestsActionFilter(IDistributedCache cache, ILogger logger)
{
_cache = cache;
_logger = logger;
}
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var factorValues = new string?[FactorNames.Length];
var isFromBody =
context.ActionDescriptor.Parameters.Any(r => r.BindingInfo?.BindingSource == BindingSource.Body);
if (isFromBody)
{
var parameterValue = context.ActionArguments.FirstOrDefault().Value;
factorValues = FactorNames.Select(name =>
parameterValue?.GetType().GetProperty(name)?.GetValue(parameterValue)?.ToString()).ToArray();
}
else
{
for (var index = 0; index < FactorNames.Length; index++)
{
if (context.ActionArguments.TryGetValue(FactorNames[index], out var factorValue))
{
factorValues[index] = factorValue?.ToString();
}
}
}
if (factorValues.All(string.IsNullOrEmpty))
{
_logger.LogWarning("Please config FactorNames.");
await next();
return;
}
var idempotentKey = $"{context.HttpContext.Request.Path.Value}:{string.Join("-", factorValues)}";
var idempotentValue = await _cache.GetStringAsync(idempotentKey);
if (idempotentValue != null)
{
_logger.LogWarning("Received duplicate request({},{}), short-circuiting...", idempotentKey, idempotentValue);
context.Result = new AcceptedResult();
}
else
{
await _cache.SetStringAsync(idempotentKey, DateTimeOffset.UtcNow.ToString(),
new DistributedCacheEntryOptions {AbsoluteExpirationRelativeToNow = AbsoluteExpirationRelativeToNow});
await next();
}
}
}

In PreventDuplicateRequestsActionFilter, we first get the value of the specified parameter field from through reflection, because the value from the request body is slightly different, we need to handle it separately; Next, start stitching the key and check redis, if the key already exists, we need to short-circuit the request, here directly returns instead of or other error status, is to avoid the upstream has already failed to call and continue to retry.ActionArgumentsAccepted (202)Conflict (409)

PreventDuplicateRequestsAttribute

The entire logic of the anti-heavy component has already been implemented in , and since it requires injection and objecting, we use to implement a custom property for ease of use.PreventDuplicateRequestsActionFilter IDistributedCacheILoggerIFilterFactory

[AttributeUsage(AttributeTargets.Method)]
public class PreventDuplicateRequestsAttribute : Attribute, IFilterFactory
{
private readonly string[] _factorNames;
private readonly int _expiredMinutes;
public PreventDuplicateRequestsAttribute(int expiredMinutes, params string[] factorNames)
{
_expiredMinutes = expiredMinutes;
_factorNames = factorNames;
}
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
var filter = serviceProvider.GetService();
filter.FactorNames = _factorNames;
filter.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(_expiredMinutes);
return filter;
}
public bool IsReusable => false;
}

enroll

For simplicity, operate redis, directly use the Microsoft Microsoft.Extensions.Caching.StackExchangeRedis package; Register, no registration required.PreventDuplicateRequestsActionFilterPreventDuplicateRequestsAttribute

builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "127.0.0.1:6379,DefaultDatabase=1";
});
builder.Services.AddScoped();

use

Suppose we have an interface where we specify OrderId and Reason as factors in the input parameters.CancelOrder

namespace PreventDuplicateRequestDemo.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class OrderController : ControllerBase
{
[HttpPost(nameof(CancelOrder))]
[PreventDuplicateRequests(5, "OrderId", "Reason")]
public async Task CancelOrder([FromBody] CancelOrderRequest request)
{
await Task.Delay(1000);
return new OkResult();
}
}
public class CancelOrderRequest
{
public Guid OrderId { get; set; }
public string Reason { get; set; }
}
}

Start the program, call the API multiple times, except for the first successful call, the rest of the requests are short-circuited

image


Check out redis, there are already records

image


Reference link

https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-7.0
 https://learn.microsoft.com/en-us/aspnet/core/performance/caching/distributed?view=aspnetcore-7.0

Source