ASP.NET Core uses Filter and Redis to implement interface anti-duplication

background

In daily development, it is often necessary to add anti-heavy functions to some key business interfaces that do not respond quickly, that is, for multiple identical requests received in a short period of time, only one will be processed, and the rest will not be processed to avoid dirty data.

This is slightly different from idempotency. Idempotency requires the same effect and result for repeated requests, and usually needs to perform business operations inside the interface. Pre-check status; and anti-duplication can be considered as a general-purpose function that has nothing to do with business. In ASP.NET Core, we can use Filter and redis to achieve it.

About Filters

The origin 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 Filter into one type, called Filter, to simplify API and improve flexibility.

Filter in ASP.NET Core can be used to implement various functions, such as authentication, logging, exception handling, performance monitoring, etc.

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

Coding implementation

The idea of the anti-heavy component is very simple. Some parameters of the first request are stored in redis as identifiers, and an expiration time is set. When the next request comes, first check whether the same redis request has been processed;

As a general component, we need to allow users to customize the fields used as identifiers and the expiration time. Let’s implement it below.

PreventDuplicateRequestsActionFilter

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

In the PreventDuplicateRequestsActionFilter, we first get the value of the specified parameter field from ActionArguments through reflection. Since the value from the request body is slightly different, we need to process it separately; then start splicing the key and check redis, if the key Already exists, we need to short-circuit the request, here directly return Accepted (202) instead of Conflict (409) or other error status, in order to avoid the failure of the upstream call Go ahead and try again.

PreventDuplicateRequestsAttribute

All the logic of the anti-duplicate component has been implemented in PreventDuplicateRequestsActionFilter, because it needs to inject IDisttributedCache and ILogger objects, we use IFilterFactory code> Implement a custom attribute for easy use.

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

register

For simplicity, to operate redis, directly use the Microsoft.Extensions.Caching.StackExchangeRedis package; register PreventDuplicateRequestsActionFilter, PreventDuplicateRequestsAttribute does not need to register.

builder.Services.AddStackExchangeRedisCache(options =></code><code>{<!-- --></code><code> options.Configuration = "127.0.0.1:6379,DefaultDatabase=1"; </code><code>});</code><code>builder.Services.AddScoped<PreventDuplicateRequestsActionFilter>();

use

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

namespace PreventDuplicateRequestDemo.Controllers</code><code>{<!-- --></code><code> [Route("api/[controller]")]</code><code> [ApiController ]</code><code> public class OrderController : ControllerBase</code><code> {<!-- --></code><code> [HttpPost(nameof(CancelOrder))]</code><code> [PreventDuplicateRequests(5, "OrderId", "Reason")]</code><code> public async Task<IActionResult> CancelOrder([FromBody] CancelOrderRequest request)</code><code> {<!-- -- ></code><code> await Task.Delay(1000);</code><code> return new OkResult();</code><code> }</code><code> }</code><code> </code><code> public class CancelOrderRequest</code><code> {<!-- --></code><code> public Guid OrderId { get; set; }</code><code> public string Reason { get; set; }</code><code> }</code><code>}</code>
Start the program, call the api multiple times, except for the first successful call, all other requests are short-circuited

View redis, there are records