Repository navigation
Architectural considerations for Idempotency Middleware, Distributed Locks, and RequestAborted in ASP.NET 8 #69761
|
Hi everyone,
|
Replies: 1 comment
|
I would approach this by separating request cancellation, business transaction consistency, and idempotency state management. They have different lifecycle and reliability requirements.
A client disconnect does not automatically terminate middleware execution. It signals cancellation, and downstream operations may throw OperationCanceledException. An awaited finally block can still perform cleanup using a short-lived, independent cancellation token: try This is an illustrative pattern, not a complete idempotency implementation. The release must be ownership-aware (for example, an atomic compare-and-delete in Redis). A plain DEL could accidentally release a lock acquired by another request after the original lease expired. The lock also needs a TTL because finally cannot guarantee execution during process termination.
This is arguably the most important distinction. EF Core forwards cancellation to the database provider, but cancellation is cooperative. If cancellation or connection failure occurs around transaction commit, the outcome may be unknown. Therefore, I would avoid automatically marking an idempotency record as Failed simply because RequestAborted was triggered. A safer model is:
When possible, persist the idempotency completion record in the same database transaction as the business changes, with a unique constraint protecting the operation identity. This makes the committed business outcome and its completion marker atomic. A Redis response cache can then be treated as a derived optimization rather than the sole source of truth.
The DI request scope is not immediately disposed merely because RequestAborted is signaled. It normally remains valid until request processing finishes. For short cleanup awaited within the middleware, using request-scoped services can be appropriate, provided their state remains usable. For work intentionally detached from the request, use a BackgroundService or a bounded queue and create a fresh scope with IServiceScopeFactory. Do not capture HttpContext or a request-scoped DbContext in fire-and-forget tasks.
Polly can help with transient failures, timeouts, and resilience policies, but it does not provide transactional guarantees or replace deterministic cleanup. I would be particularly cautious about retrying the complete HTTP operation. If the first attempt committed successfully but its result was lost, replaying the handler could duplicate business effects unless those operations are independently idempotent. My preferred architecture Use a distributed lock only as a short-lived concurrency optimization, with ownership tokens and expiration. Let the persistent idempotency record enforce replay safety, and use a bounded, request-independent cancellation budget for cleanup. For operations whose outcome remains uncertain, reconcile the persisted business state before allowing another attempt. The essential invariant is: A client disconnect must never turn an already committed business operation into an apparently unprocessed request. Relevant references:
|
I would approach this by separating request cancellation, business transaction consistency, and idempotency state management. They have different lifecycle and reliability requirements.
A client disconnect does not automatically terminate middleware execution. It signals cancellation, and downstream operations may throw OperationCanceledException.
An awaited finally block can still perform cleanup using a short-lived, independent cancellation token:
try
{
await next(context);
}
finally
{
using var cleanupCts =
new CancellationTokenSource(TimeSpan.FromSeconds(3));
try
{
await distributedLock.ReleaseAsync(
cleanupCts.Token);
}
catch (Exception ex)
{
logger.LogEr…