Round #332
08 May 2026, 16:20· csharp· qwen2.5-coder-7b-instruct· r13-clean-hints
- Complexity
- band 0
- ZP / UO / OBS
- 4 / 0 / 0
- Journey weight
- 291
- Lightshift ΔL
- +0.00
Prompt
Implement idempotency key middleware that deduplicates POST requests
Zero-point index (what the engine surfaced)
ZPI-ZPI-0002 | 2026-05-08T16:20:39Z
Implement idempotency key middleware that deduplicates POST requests
ZERO POINT — 4 entries (structural invariants)
● hsl(314.84, 100%, 30%) IMiddleware [contract]
● hsl(280.69, 100%, 30%) RequestDelegate [contract]
● hsl(329.61, 100%, 19%) IDistributedCache [contract]
● hsl(136.10, 100%, 30%) DbContext [contract]
UNOBSERVED — 0 entries (implied truths)
OBSERVED — 0 entries (explicit in request)
COMPLEXITY │ 4 entries │ spread: 0° │ band: LOW
Baseline response (no ZPI)
# Idempotency Key Middleware Implementation for ASP.NET Core
## Architecture Overview
```mermaid
graph TD
A[Incoming POST Request] --> B{Idempotency Header Present?}
B -- No --> C[Process Normally]
B -- Yes --> D{Key Exists in Cache?}
D -- Yes --> E[Return Cached Response]
D -- No --> F{Request Processed Successfully?}
F -- Yes --> G[Store Key + Response in Cache]
F -- No --> H[Log Error, Don't Store]
```
## Implementation Files
### 1. IdempotencyOptions.cs - Configuration
```csharp
namespace IdempotencyMiddleware;
public class IdempotencyOptions
{
public const string DefaultHeaderName = "Idempotency-Key";
// Cache configuration
public int CacheSize { get; set; } = 10_000;
public TimeSpan CacheExpiration { get; set; } = TimeSpan.FromHours(24);
public bool EnableCacheCleanup { get; set; } = true;
// Storage configuration
public string? RedisConnectionString { get; set; }
public string? DatabaseTableName { get; set; } = "IdempotencyKeys";
// Behavior configuration
public int MaxRetriesOnCacheMiss { get; set; } = 3;
public TimeSpan RetryDelay { get; set; } = TimeSpan.FromSeconds(1);
}
```
### 2. IIdempotencyService.cs - Interface
```csharp
namespace IdempotencyMiddleware;
public interface IIdempotencyService
{
Task<IdempotencyResult> GetOrCreateAsync(string key,
Func<Task<string>> requestProcessor,
CancellationToken cancellationToken = default);
void CleanupExpiredEntries();
}
public class IdempotencyResult
{
public bool IsCached { get; set; }
public string? CachedResponse { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime ExpiresAt { get; set; }
}
```
### 3. IdempotencyService.cs - Service Implementation
```csharp
namespace IdempotencyMiddleware;
public class IdempotencyService : IIdempotencyService
{
private readonly string _cacheKeyPrefix = "idempotency:";
private readonly int _defaultCacheSize = 10_000;
public async Task<IdempotencyResult> GetOrCreateAsync(
string key,
Func<Task<string>> requestProcessor,
CancellationToken cancellationToken = default)
{
var cacheKey = $"{_cacheKeyPrefix}{key}";
// Try to get cached response first
var cachedResponse = await _cache.Get(cacheKey, async () =>
{
try
{
return await requestProcessor();
}
catch (Exception ex) when (!ex.IsCancellationRequested)
{
throw;
}
}, cancellationToken);
if (cachedResponse != null)
{
return new IdempotencyResult
{
IsCached = true,
CachedResponse = cachedResponse,
CreatedAt = DateTime.UtcNow,
ExpiresAt = DateTime.UtcNow.Add(_cacheExpiration)
};
}
// Process the request normally
var response = await requestProcessor();
// Store in cache for future requests
await _cache.Set(cacheKey, response, _cacheExpiration);
return new IdempotencyResult
{
IsCached = false,
CachedResponse = null,
CreatedAt = DateTime.UtcNow,
ExpiresAt = DateTime.UtcNow.Add(_cacheExpiration)
};
}
public void CleanupExpiredEntries()
{
// Implement cleanup logic based on your storage
// For Redis: KEYS idempotency:* -EXPIRE 0
}
}
```
### 4. IdempotencyMiddleware.cs - Middleware Class
```csharp
namespace IdempotencyMiddleware;
public class IdempotencyMiddleware
{
private readonly RequestDelegate _next;
private readonly IIdempotencyService _idempotencyService;
private readonly IdempotencyOptions _options;
public IdempotencyMiddleware(
RequestDelegate next,
IIdempotencyService idempotencyService,
IdempotencyOptions options = null)
{
_next = next;
_idempotencyService = idempotencyService;
_options = options ?? new IdempotencyOptions();
}
public async Task InvokeAsync(HttpContext context, CancellationToken cancellationToken)
{
// Only apply to POST/PUT/PATCH requests with body
if (!context.Request.Method.Equals("POST", StringComparison.OrdinalIgnoreCase) &&
!context.Request.Method.Equals("PUT", StringComparison.OrdinalIgnoreCase) &&
!context.Request.Method.Equals("PATCH", StringComparison.OrdinalIgnoreCase))
{
await _next(context, cancellationToken);
return;
}
// Extract idempotency key from header
var idempotencyKey = context.Request.Headers[_options.DefaultHeaderName].FirstOrDefault();
if (string.IsNullOrEmpty(idempotencyKey))
{
// No idempotency key - process normally
await _next(context, cancellationToken);
return;
}
try
{
var requestProcessor = async () =>
{
context.Response.Body.Position = 0;
using (var reader = new StreamReader(context.Request.Body))
{
return await reader.ReadToEndAsync();
}
};
var result = await _idempotencyService.GetOrCreateAsync(
idempotencyKey,
requestProcessor,
cancellationToken);
if (result.IsCached)
{
// Return cached response with appropriate headers
context.Response.StatusCode = 200;
context.Response.Headers["X-Idempotency-Key"] = idempotencyKey;
var cachedResponse = result.CachedResponse ??
await _cache.Get(result.CreatedAt, async () =>
await requestProcessor());
await context.Response.WriteAsync(cachedResponse);
}
else
{
// Process normally and store for future requests
await _next(context, cancellationToken);
}
}
catch (Exception ex) when (!ex.IsCancellationRequested)
{
// Log error but don't fail the request
_logger.LogError(ex, "Idempotency middleware error: {Message}", ex.Message);
context.Response.StatusCode = 500;
await context.Response.WriteAsync("Internal server error");
}
}
private readonly ILogger<IdempotencyMiddleware> _logger;
}
```
### 5. IdempotencyServiceExtensions.cs - Service Registration
```csharp
namespace IdempotencyMiddleware;
public static class IdempotencyServiceExtensions
{
public static IServiceCollection AddIdempotency(
this IServiceCollection services,
Action<IdempotencyOptions> options = null)
{
var idempotencyOptions = new IdempotencyOptions();
options?.Invoke(idempotencyOptions);
// Register service with default implementation
services.AddSingleton<IIdempotencyService, DefaultIdempotencyService>();
// Configure Redis if provided
if (!string.IsNullOrEmpty(idempotencyOptions.RedisConnectionString))
{
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = idempotencyOptions.RedisConnectionString;
options.InstanceName = "Idempotency";
});
}
return services;
}
public static IApplicationBuilder UseIdempotency(
this IApplicationBuilder builder,
IdempotencyOptions options = null)
{
var idempotencyOptions = options ?? new IdempotencyOptions();
// Register middleware with options
return builder;
}
}
```
### 6. DefaultIdempotencyService.cs - Redis Implementation
```csharp
namespace IdempotencyMiddleware;
public class DefaultIdempotencyService : IIdempotencyService
{
private readonly IDistributedCache _cache;
private readonly string _prefix = "idempotency:";
public DefaultIdempotencyService(IDistributedCache cache)
{
_cache = cache;
}
public async Task<IdempotencyResult> GetOrCreateAsync(
string key,
Func<Task<string>> requestProcessor,
CancellationToken cancellationToken = default)
{
var cacheKey = $"{_prefix}{key}";
// Try to get cached response first
var cachedResponse = await _cache.GetStringAsync(cacheKey, cancellationToken);
if (!string.IsNullOrEmpty(cachedResponse))
{
return new IdempotencyResult
{
IsCached = true,
CachedResponse = cachedResponse,
CreatedAt = DateTime.UtcNow,
ExpiresAt = DateTime.UtcNow.Add(_cacheExpiration)
};
}
// Process the request normally
var response = await requestProcessor();
// Store in cache for future requests
await _cache.SetStringAsync(cacheKey, response,
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = _cacheExpiration
});
return new IdempotencyResult
{
IsCached = false,
CachedResponse = null,
CreatedAt = DateTime.UtcNow,
ExpiresAt = DateTime.UtcNow.Add(_cacheExpiration)
};
}
private TimeSpan _cacheExpiration =>
new IdempotencyOptions().CacheExpiration;
}
```
### 7. Program.cs - Application Setup Example
```csharp
var builder = WebApplication.CreateBuilder(args);
// Add services
builder.Services.AddIdempotency(options =>
{
options.CacheSize = 10_000;
options.CacheExpiration = TimeSpan.FromHours(24);
// Configure Redis (optional)
if (!string.IsNullOrEmpty(builder.Configuration["Redis:ConnectionString"]))
{
options.RedisConnectionString = builder.Configuration["Redis:ConnectionString"];
}
});
// Add other services...
builder.Services.AddControllers();
var app = builder.Build();
// Use idempotency middleware for specific routes
app.MapPost("/api/orders", async (HttpContext context) =>
{
// Your order creation logic here
var response = await ProcessOrderAsync(context);
return Results.Ok(response);
})
.UseIdempotency();
app.Run();
```
### 8. IdempotencyController.cs - Example Controller
```csharp
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly ILogger<OrdersController> _logger;
[HttpPost]
public async Task<IActionResult> CreateOrder([FromBody] OrderDto order)
{
// Extract idempotency key from header
var idempotencyKey = Request.Headers["Idempotency-Key"].FirstOrDefault();
if (string.IsNullOrEmpty(idempotencyKey))
{
return BadRequest("Missing Idempotency-Key header");
}
try
{
// Process the order
var result = await ProcessOrderAsync(order);
// Store response for idempotency
await _cache.Set(
$"order:{idempotencyKey}",
JsonSerializer.Serialize(result),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24)
});
return Ok(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing order: {Order}", order.Id);
return StatusCode(500, new { error = "Internal server error" });
}
}
}
```
## Key Features Summary
| Feature | Description |
|---------|-------------|
| **Header-based** | Uses `Idempotency-Key` header for identification |
| **Cache-first** | Checks cache before processing request |
| **Automatic storage** | Stores successful responses automatically |
| **Configurable TTL** | 24-hour default expiration with options |
| **Redis support** | Distributed cache via Redis connection string |
| **Error handling** | Graceful degradation on failures |
| **Route-specific** | Can be applied to specific endpoints only |
## Usage Example
```csharp
// Client-side usage:
POST /api/orders HTTP/1.1
Host: api.example.com
Content-Type: application/json
Idempotency-Key: abc-123-def-456
{
"amount": 99.99,
"items": [...]
}
// Response (first request):
HTTP/1.1 200 OK
X-Idempotency-Key: abc-123-def-456
{"orderId": "order-789", "status": "created"}
// Same key, second request (network retry scenario):
HTTP/1.1 200 OK
X-Idempotency-Key: abc-123-def-456
{"orderId": "order-789", "status": "created"}
```
Enhanced response (with ZPI)
# Idempotency Key Middleware Implementation
## Architecture Decisions
1. **IDistributedCache** is the ideal choice over IMiddleware because:
- It provides distributed storage across multiple server instances
- Built-in expiration support via `AbsoluteExpiration`
- Thread-safe concurrent access
- No need for custom middleware registration logic
2. **Middleware approach**: Using a standard ASP.NET Core middleware pipeline component rather than the older IMiddleware interface, which is deprecated in modern .NET versions.
3. **Key design choices**:
- Configurable TTL (default 1 hour) to balance memory usage vs. idempotency window
- Key prefix for namespacing different API endpoints
- Response caching to avoid reprocessing identical requests
- Graceful degradation when cache is unavailable
## Implementation
```csharp
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Options;
namespace IdempotencyMiddleware
{
/// <summary>
/// Configuration options for idempotency middleware behavior.
/// </summary>
public class IdempotencyOptions
{
/// <summary>
/// Default time-to-live for idempotency keys (1 hour).
/// </summary>
public TimeSpan DefaultTtl { get; set; } = TimeSpan.FromHours(1);
/// <summary>
/// Prefix added to all stored keys.
/// </summary>
public string KeyPrefix { get; set; } = "idempotency:";
/// <summary>
/// Whether to cache the response body for identical requests.
/// </summary>
public bool CacheResponse { get; set; } = true;
/// <summary>
/// Maximum number of concurrent operations allowed per key.
/// Set to 1 for strict idempotency, >1 for parallel processing.
/// </summary>
public int MaxConcurrentOperations { get; set; } = 1;
}
/// <summary>
/// Middleware that enforces request deduplication using idempotency keys.
/// Extracts the key from headers or query parameters and checks cache before processing.
/// </summary>
public class IdempotencyMiddleware
{
private readonly RequestDelegate _next;
private readonly IDistributedCache _cache;
private readonly IdempotencyOptions _options;
public IdempotencyMiddleware(
RequestDelegate next,
IDistributedCache cache,
IOptions<IdempotencyOptions> options)
{
_next = next;
_cache = cache;
_options = options.Value;
}
/// <summary>
/// Extracts the idempotency key from request headers or query parameters.
/// Falls back to a generated UUID if no key is provided.
/// </summary>
private string GetIdempotencyKey(HttpRequest request)
{
// Check header first (recommended for REST APIs)
var headerKey = request.Headers["X-Idempotency-Key"].FirstOrDefault();
if (!string.IsNullOrEmpty(headerKey))
return headerKey;
// Check query parameter (for backward compatibility or GET requests)
var queryKey = request.Query["idempotency_key"].FirstOrDefault();
if (!string.IsNullOrEmpty(queryKey))
return queryKey;
// Generate a unique key for this specific request
return Guid.NewGuid().ToString("N");
}
/// <summary>
/// Checks if the idempotency key exists in cache and returns cached response.
/// </summary>
private async Task<IdempotencyResult?> GetCachedResponse(
string key,
HttpRequest request)
{
var cacheKey = $"{_options.KeyPrefix}{key}";
try
{
var data = await _cache.GetStringAsync(cacheKey);
if (string.IsNullOrEmpty(data))
return null;
// Parse cached response
var parts = data.Split('|');
if (parts.Length < 2)
return null;
var statusCode = int.Parse(parts[0]);
var responseBody = parts.Skip(1).Aggregate("", (a, b) => a + b);
return new IdempotencyResult(statusCode, responseBody);
}
catch
{
// Cache unavailable - don't fail the request
return null;
}
}
/// <summary>
/// Stores the idempotency key and response in cache.
/// </summary>
private async Task StoreResponse(
string key,
int statusCode,
string responseBody)
{
var cacheKey = $"{_options.KeyPrefix}{key}";
try
{
// Format: StatusCode|ResponseBody
var data = $"{statusCode}|{responseBody}";
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = _options.DefaultTtl,
SlidingExpiration = TimeSpan.FromMinutes(5)
};
await _cache.SetStringAsync(cacheKey, data, options);
}
catch
{
// Cache unavailable - don't fail the request
}
}
/// <summary>
/// Main middleware logic: check cache, process if needed, store result.
/// </summary>
public async Task InvokeAsync(HttpContext context)
{
var key = GetIdempotencyKey(context.Request);
var cachedResult = await GetCachedResponse(key, context.Request);
// Return cached response if available
if (cachedResult != null)
{
context.Response.StatusCode = cachedResult.StatusCode;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(cachedResult.ResponseBody);
return;
}
// Process the request normally
var originalHandler = context.Features.Get<IHttpPipelineFeature>();
if (originalHandler != null)
{
// Use pipeline feature to get response
var responseStream = new MemoryStream();
await _next(context);
// Capture response for caching
var responseContent = await context.Response.Body.CopyToAsync(responseStream);
var responseBody = Encoding.UTF8.GetString(responseStream.ToArray());
// Store in cache after successful processing
if (_options.CacheResponse)
await StoreResponse(key, context.Response.StatusCode, responseBody);
}
else
{
// Fallback for older ASP.NET Core versions
await _next(context);
var responseStream = new MemoryStream();
await context.Response.Body.CopyToAsync(responseStream);
var responseBody = Encoding.UTF8.GetString(responseStream.ToArray());
if (_options.CacheResponse)
await StoreResponse(key, context.Response.StatusCode, responseBody);
}
}
}
/// <summary>
/// Represents a cached idempotency result.
/// </summary>
public class IdempotencyResult
{
public int StatusCode { get; set; }
public string ResponseBody { get; set; } = string.Empty;
public IdempotencyResult(int statusCode, string responseBody)
{
StatusCode = statusCode;
ResponseBody = responseBody;
}
}
/// <summary>
/// Extension method for registering idempotency middleware.
/// </summary>
public static class IdempotencyMiddlewareExtensions
{
/// <summary>
/// Adds idempotency middleware to the ASP.NET Core pipeline.
/// </summary>
public static IApplicationBuilder UseIdempotency(
this IApplicationBuilder builder,
Action<IdempotencyOptions>? configure = null)
{
var options = new IdempotencyOptions();
if (configure != null)
configure(options);
return builder.UseMiddleware<IdempotencyMiddleware>(options);
}
}
}
```
## Usage Example
```csharp
// Program.cs or Startup.cs
var builder = WebApplication.CreateBuilder(args);
// Register distributed cache (Redis, MemoryCache, etc.)
builder.Services.AddDistributedMemoryCache();
// Or: builder.Services.AddStackExchangeRedisCache(options => { ... });
// Configure idempotency middleware options
builder.Services.Configure<IdempotencyOptions>(options =>
{
options.DefaultTtl = TimeSpan.FromHours(2);
options.KeyPrefix = "api:";
});
var app = builder.Build();
// Add idempotency middleware before your API routes
app.UseIdempotency(options =>
{
// Custom configuration if needed
});
// Your API routes
app.MapPost("/orders", async (HttpContext context) =>
{
// This request will be deduplicated based on X-Idempotency-Key header
var order = new Order { /* ... */ };
return Results.Ok(order);
});
app.Run();
```
## How It Works
1. **Request arrives** → Middleware extracts idempotency key from headers or query parameters
2. **Cache lookup** → If key exists, returns cached response immediately (no processing)
3. **Process request** → Normal pipeline execution occurs for new requests
4. **Store result** → Response is cached with TTL expiration for future identical requests
## Key Features
- ✅ **Distributed cache support** via `IDistributedCache` interface
- ✅ **Configurable TTL** to control idempotency window duration
- ✅ **Response caching** prevents duplicate processing entirely
- ✅ **Graceful degradation** when cache is unavailable
- ✅ **Namespaced keys** prevent conflicts between different APIs
Grader chain
Each grader reads the same baseline + enhanced pair and casts an independent verdict. Disagreement is the framework's own signal — not noise to be averaged away.
V1claude-sonnet-4-6
WorseV4qwen2.5-coder-7b-instruct
BetterComments
Our grader said what it said. What do you say? Comment as a guest below.
No comments yet. Be the first to say what you make of this round.