🔒 ملاحظات حرفهای: شرط رقابت (Race Condition)
یک پنجره کوچک شرط رقابت (Race Condition) بین بررسی کش و تنظیم آن وجود دارد. برای سازگاری مطلق، باید الگوی قفل توزیع شده (Distributed Lock) را در نظر بگیریم، اگرچه این کار باعث افزایش پیچیدگی و تأخیر (Latency) خواهد شد. ⚠️
🚀 اعمال Attribute به اکشنهای کنترلر
اکنون، میتوانیم این Attribute را به اکشنهای کنترلر خود اعمال کنیم:
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
[HttpPost]
[Idempotent(cacheTimeInMinutes: 60)]
public IActionResult CreateOrder([FromBody] CreateOrderRequest request)
{
// Process the order...
return CreatedAtAction(nameof(GetOrder), new { id = orderDto.Id }, orderDto);
}
}
✨ همتوانی با Minimal APIs
برای پیادهسازی همتوانی با Minimal APIs، میتوانیم از IEndpointFilter استفاده کنیم. 🛠
📜 کد: IdempotencyFilter
internal sealed class IdempotencyFilter(int cacheTimeInMinutes = 60)
: IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
// Parse the Idempotence-Key header from the request
if (TryGetIdempotenceKey(out Guid idempotenceKey))
{
return Results.BadRequest("Invalid or missing Idempotence-Key header");
}
IDistributedCache cache = context.HttpContext
.RequestServices.GetRequiredService<IDistributedCache>();
// Check if we already processed this request and return a cached response (if it exists)
string cacheKey = $"Idempotent_{idempotenceKey}";
string? cachedResult = await cache.GetStringAsync(cacheKey);
if (cachedResult is not null)
{
IdempotentResponse response = JsonSerializer.Deserialize<IdempotentResponse>(cachedResult)!;
return new IdempotentResult(response.StatusCode, response.Value);
}
object? result = await next(context);
// Execute the request and cache the response for the specified duration
if (result is IStatusCodeHttpResult { StatusCode: >= 200 and < 300 } statusCodeResult
and IValueHttpResult valueResult)
{
int statusCode = statusCodeResult.StatusCode ?? StatusCodes.Status200OK;
IdempotentResponse response = new(statusCode, valueResult.Value);
await cache.SetStringAsync(
cacheKey,
JsonSerializer.Serialize(response),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(cacheTimeInMinutes)
}
);
}
return result;
}
}
// We have to implement a custom result to write the status code
internal sealed class IdempotentResult : IResult
{
private readonly int _statusCode;
private readonly object? _value;
public IdempotentResult(int statusCode, object? value)
{
_statusCode = statusCode;
_value = value;
}
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.StatusCode = _statusCode;
return httpContext.Response.WriteAsJsonAsync(_value);
}
}
🔗 اعمال فیلتر به اِندپوینت
اکنون، میتوانیم این فیلتر اِندپوینت را به اِندپوینت Minimal API خود اعمال کنیم:
app.MapPost("/api/orders", CreateOrder)
.RequireAuthorization()
.WithOpenApi()
.AddEndpointFilter<IdempotencyFilter>();💡 جایگزین دیگر
یک جایگزین برای دو پیادهسازی قبلی (Attribute و Endpoint Filter) پیادهسازی منطق همتوانی در یک Middleware سفارشی است. 🌐