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.
By using Filter, we can run custom code before or after a specific stage of the request processing pipeline to achieve AOP effect.
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.ActionArguments
Accepted (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
IDistributedCache
ILogger
IFilterFactory
[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.PreventDuplicateRequestsActionFilter
PreventDuplicateRequestsAttribute
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
Check out redis, there are already records
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