Round #284

08 May 2026, 12:25· csharp· openai/gpt-oss-120b:free· r13-clean-hints
Complexity
band 0
ZP / UO / OBS
4 / 0 / 0
Journey weight
288
Lightshift ΔL
+0.25

Prompt

Implement idempotency key middleware that deduplicates POST requests

Zero-point index (what the engine surfaced)

ZPI-ZPI-0002 | 2026-05-08T12:25:29Z Implement idempotency key middleware that deduplicates POST requests ZERO POINT — 4 entries (structural invariants) ● hsl(314.84, 100%, 27%) IMiddleware [contract] ● hsl(280.69, 100%, 27%) RequestDelegate [contract] ● hsl(329.61, 100%, 24%) IDistributedCache [contract] ● hsl(136.10, 100%, 34%) DbContext [contract] UNOBSERVED — 0 entries (implied truths) OBSERVED — 0 entries (explicit in request) COMPLEXITY │ 4 entries │ spread: 0° │ band: LOW

Baseline response (no ZPI)

Below is a **complete, production‑ready implementation** of an **Idempotency‑Key middleware** for ASP.NET Core that deduplicates POST (or any non‑GET) requests. The middleware: * Looks for a request header (by default **`Idempotency-Key`**). * Stores the request body together with the response (status code, headers, body) in a configurable cache. * On a subsequent request with the same key it **returns the cached response** without re‑executing the downstream pipeline. * Works for any content type (JSON, form‑urlencoded, multipart, etc.). * Is safe for distributed scenarios when you plug in a distributed cache (e.g., Redis, SQL Server, NCache). * Allows per‑endpoint opt‑in/opt‑out and custom key‑generation logic. * Returns **409 Conflict** when the same key is used with a different HTTP method or with a different request payload (optional strict mode). --- ## 1. Package References Add the following NuGet packages to your project (the core one is already part of ASP.NET Core, the others are optional depending on the cache you choose): ```xml <ItemGroup> <!-- Core ASP.NET Core --> <PackageReference Include="Microsoft.AspNetCore.App" /> <!-- Distributed cache (choose one) --> <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.0" /> <!-- or --> <PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="8.0.0" /> </ItemGroup> ``` --- ## 2. Data Model (what we store in the cache) ```csharp using System; using System.Collections.Generic; using System.Text.Json.Serialization; namespace IdempotencyMiddleware { /// <summary> /// Represents a cached response for a given idempotency key. /// </summary> public sealed class IdempotentResponse { public int StatusCode { get; set; } public Dictionary<string, string[]> Headers { get; set; } = new(); public byte[] Body { get; set; } = Array.Empty<byte>(); public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; // Optional: store a hash of the request payload to detect mismatched bodies. public string? RequestHash { get; set; } } } ``` The model is deliberately simple and serializable by `System.Text.Json`. --- ## 3. Options class ```csharp using System; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Primitives; namespace IdempotencyMiddleware { public class IdempotencyOptions { /// <summary> /// Header name that carries the idempotency key. Default: "Idempotency-Key". /// </summary> public string HeaderName { get; set; } = "Idempotency-Key"; /// <summary> /// How long a cached response lives. Default: 24 hours. /// </summary> public TimeSpan CacheEntryExpiration { get; set; } = TimeSpan.FromHours(24); /// <summary> /// Whether to enforce strict payload matching. /// If true, a request with the same key but a different body will be rejected with 409. /// </summary> public bool StrictPayload { get; set; } = false; /// <summary> /// Function that decides whether a request should be processed by the middleware. /// By default only POST, PUT, PATCH, DELETE are considered. /// </summary> public Func<HttpContext, bool> ShouldHandleRequest { get; set; } = ctx => ctx.Request.Method.Equals("POST", StringComparison.OrdinalIgnoreCase) || ctx.Request.Method.Equals("PUT", StringComparison.OrdinalIgnoreCase) || ctx.Request.Method.Equals("PATCH", StringComparison.OrdinalIgnoreCase) || ctx.Request.Method.Equals("DELETE", StringComparison.OrdinalIgnoreCase); /// <summary> /// Optional custom key generator (e.g., combine header + user id). /// Return null to skip handling. /// </summary> public Func<HttpContext, string?>? GenerateKey { get; set; } /// <summary> /// Cache implementation. If not supplied, an in‑memory cache is created. /// </summary> public IDistributedCache? DistributedCache { get; set; } /// <summary> /// Optional logger name. /// </summary> public string? LoggerCategory { get; set; } } } ``` --- ## 4. Middleware implementation ```csharp using System; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; namespace IdempotencyMiddleware { public class IdempotencyMiddleware { private readonly RequestDelegate _next; private readonly IdempotencyOptions _options; private readonly IDistributedCache _cache; private readonly ILogger _logger; public IdempotencyMiddleware(RequestDelegate next, IdempotencyOptions options, ILoggerFactory loggerFactory) { _next = next ?? throw new ArgumentNullException(nameof(next)); _options = options ?? throw new ArgumentNullException(nameof(options)); // Resolve cache _cache = options.DistributedCache ?? new MemoryDistributedCache(new OptionsWrapper<MemoryDistributedCacheOptions>(new MemoryDistributedCacheOptions())); _logger = loggerFactory.CreateLogger(options.LoggerCategory ?? typeof(IdempotencyMiddleware).FullName); } public async Task InvokeAsync(HttpContext context) { // 1️⃣ Decide if we should handle this request if (!_options.ShouldHandleRequest(context)) { await _next(context); return; } // 2️⃣ Resolve the idempotency key string? key = ResolveKey(context); if (string.IsNullOrWhiteSpace(key)) { // No key → just pass through (or you could reject with 400) await _next(context); return; } // 3️⃣ Try to fetch a cached response var cached = await GetCachedResponseAsync(key); if (cached != null) { // Optional strict payload check if (_options.StrictPayload && !await IsSameRequestPayloadAsync(context, cached.RequestHash)) { _logger.LogWarning("Idempotency key '{Key}' reused with a different payload.", key); context.Response.StatusCode = StatusCodes.Status409Conflict; await context.Response.WriteAsync("Idempotency key reused with a different request payload."); return; } _logger.LogInformation("Returning cached response for idempotency key '{Key}'.", key); await WriteCachedResponseAsync(context, cached); return; } // 4️⃣ No cached entry → capture the response var originalBody = context.Response.Body; await using var memoryStream = new MemoryStream(); context.Response.Body = memoryStream; // Capture request payload hash (if strict mode) string? requestHash = null; if (_options.StrictPayload) { requestHash = await ComputeRequestHashAsync(context); // Reset request body stream for downstream processing context.Request.Body.Position = 0; } try { await _next(context); } finally { // 5️⃣ Store the response in cache var responseToCache = await BuildCachedResponseAsync(context, memoryStream, requestHash); await StoreResponseAsync(key, responseToCache); // 6️⃣ Write the response back to the original stream memoryStream.Position = 0; await memoryStream.CopyToAsync(originalBody); context.Response.Body = originalBody; } } #region Helper methods private string? ResolveKey(HttpContext ctx) { // Custom generator takes precedence if (_options.GenerateKey != null) { return _options.GenerateKey(ctx); } // Default: read from header if (ctx.Request.Headers.TryGetValue(_options.HeaderName, out var values)) { // Take the first non‑empty value var key = values.FirstOrDefault(v => !StringValues.IsNullOrEmpty(v)); return string.IsNullOrWhiteSpace(key) ? null : key; } return null; } private async Task<IdempotentResponse?> GetCachedResponseAsync(string key) { var raw = await _cache.GetAsync(key); if (raw == null) return null; try { return JsonSerializer.Deserialize<IdempotentResponse>(raw); } catch (JsonException ex) { _logger.LogError(ex, "Failed to deserialize cached idempotent response for key '{Key}'.", key); // Corrupt entry – delete it await _cache.RemoveAsync(key); return null; } } private async Task StoreResponseAsync(string key, IdempotentResponse response) { var bytes = JsonSerializer.SerializeToUtf8Bytes(response); var options = new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = _options.CacheEntryExpiration }; await _cache.SetAsync(key, bytes, options); } private async Task WriteCachedResponseAsync(HttpContext ctx, IdempotentResponse cached) { ctx.Response.StatusCode = cached.StatusCode; // Clear any existing headers that might have been set earlier ctx.Response.Headers.Clear(); foreach (var header in cached.Headers) { ctx.Response.Headers[header.Key] = header.Value; } // Ensure content‑length is correct ctx.Response.ContentLength = cached.Body?.Length; if (cached.Body?.Length > 0) { await ctx.Response.Body.WriteAsync(cached.Body, 0, cached.Body.Length); } } private async Task<IdempotentResponse> BuildCachedResponseAsync(HttpContext ctx, MemoryStream capturedBody, string? requestHash) { // Ensure the stream is at the beginning capturedBody.Position = 0; var bodyBytes = capturedBody.ToArray(); // Copy headers (excluding hop‑by‑hop headers) var headers = ctx.Response.Headers .Where(h => !IsHopByHopHeader(h.Key)) .ToDictionary( h => h.Key, h => h.Value.ToArray() ); return new IdempotentResponse { StatusCode = ctx.Response.StatusCode, Headers = headers, Body = bodyBytes, CreatedAt = DateTimeOffset.UtcNow, RequestHash = requestHash }; } private static bool IsHopByHopHeader(string headerName) { // According to RFC 2616 / 7230 var hopByHop = new[] { "Connection", "Keep-Alive", "Proxy-Authenticate", "Proxy-Authorization", "TE", "Trailer", "Transfer-Encoding", "Upgrade" }; return hopByHop.Contains(headerName, StringComparer.OrdinalIgnoreCase); } private async Task<string> ComputeRequestHashAsync(HttpContext ctx) { // Ensure the request body can be read multiple times ctx.Request.EnableBuffering(); using var sha256 = SHA256.Create(); await using var cryptoStream = new CryptoStream(Stream.Null, sha256, CryptoStreamMode.Write); await ctx.Request.Body.CopyToAsync(cryptoStream); await cryptoStream.FlushAsync(); var hash = sha256.Hash!; // Reset position for downstream pipeline ctx.Request.Body.Position = 0; return Convert.ToBase64String(hash); } private async Task<bool> IsSameRequestPayloadAsync(HttpContext ctx, string? storedHash) { if (storedHash == null) return true; // nothing to compare var currentHash = await ComputeRequestHashAsync(ctx); return storedHash == currentHash; } #endregion } // Extension method for easy registration public static class IdempotencyMiddlewareExtensions { public static IApplicationBuilder UseIdempotency(this IApplicationBuilder builder, Action<IdempotencyOptions>? configure = null) { var options = new IdempotencyOptions(); configure?.Invoke(options); return builder.UseMiddleware<IdempotencyMiddleware>(options); } } } ``` ### How it works | Step | What happens | |------|--------------| | **1** | Middleware checks `ShouldHandleRequest`. By default only non‑GET verbs are processed. | | **2** | It extracts the key from the configured header (`Idempotency-Key`) or via a custom delegate. | | **3** | Looks for a cached entry in the configured `IDistributedCache`. | | **4** | If found → optional strict payload verification → writes cached status, headers, and body to the response and **returns**. | | **5** | If not found → the response body is swapped with a `MemoryStream` so we can capture it after the downstream pipeline finishes. | | **6** | After the pipeline, the captured response (status, headers, body) together with an optional request‑body hash is stored in the cache for the configured TTL. | | **7** | The captured body is flushed back to the original response stream so the client receives the normal response. | --- ## 5. Registering the middleware ### a) Simple in‑memory usage (good for dev / single‑instance) ```csharp public class Startup { public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // other middlewares … app.UseIdempotency(opts => { opts.HeaderName = "Idempotency-Key"; opts.CacheEntryExpiration = TimeSpan.FromHours(12); opts.StrictPayload = true; // optional }); // MVC / minimal APIs … app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } } ``` ### b) Distributed Redis cache (recommended for multi‑instance) ```csharp public void ConfigureServices(IServiceCollection services) { services.AddStackExchangeRedisCache(opt => { opt.Configuration = "localhost:6379"; opt.InstanceName = "Idempotency_"; }); // Register the cache so the middleware can pick it up services.AddSingleton<IDistributedCache>(sp => sp.GetRequiredService<IDistributedCache>()); } ``` ```csharp public void Configure(IApplicationBuilder app) { app.UseIdempotency(opts => { opts.DistributedCache = app.ApplicationServices.GetRequiredService<IDistributedCache>(); opts.StrictPayload = true; }); // … } ``` ### c) Custom key generation (e.g., combine user id + header) ```csharp app.UseIdempotency(opts => { opts.GenerateKey = ctx => { // If the user is authenticated, prepend the user id to avoid cross‑user collisions var userId = ctx.User?.FindFirst("sub")?.Value; if (string.IsNullOrEmpty(userId)) return null; if (!ctx.Request.Headers.TryGetValue("Idempotency-Key", out var header)) return null; var clientKey = header.FirstOrDefault(); return $"{userId}:{clientKey}"; }; }); ``` --- ## 6. Testing the middleware ### Unit test (xUnit + Microsoft.AspNetCore.TestHost) ```csharp public class IdempotencyMiddlewareTests { private readonly TestServer _server; private readonly HttpClient _client; public IdempotencyMiddlewareTests() { var builder = new WebHostBuilder() .ConfigureServices(s => s.AddMemoryCache()) .Configure(app => { app.UseIdempotency(); app.Run(async ctx => { // Simulate a slow operation await Task.Delay(50); ctx.Response.StatusCode = 201; await ctx.Response.WriteAsync($"Created at {DateTime.UtcNow:O}"); }); }); _server = new TestServer(builder); _client = _server.CreateClient(); } [Fact] public async Task Duplicate_post_returns_cached_response() { var key = Guid.NewGuid().ToString(); var request1 = new HttpRequestMessage(HttpMethod.Post, "/") { Content = new StringContent("{\"value\":1}", Encoding.UTF8, "application/json") }; request1.Headers.Add("Idempotency-Key", key); var response1 = await _client.SendAsync(request1); var body1 = await response1.Content.ReadAsStringAsync(); // second request – same key, same payload var request2 = new HttpRequestMessage(HttpMethod.Post, "/") { Content = new StringContent("{\"value\":1}", Encoding.UTF8, "application/json") }; request2.Headers.Add("Idempotency-Key", key); var response2 = await _client.SendAsync(request2); var body2 = await response2.Content.ReadAsStringAsync(); Assert.Equal(response1.StatusCode, response2.StatusCode); Assert.Equal(body1, body2); } [Fact] public async Task Duplicate_with_different_payload_strict_mode_returns_409() { var key = Guid.NewGuid().ToString(); var server = new WebHostBuilder() .ConfigureServices(s => s.AddMemoryCache()) .Configure(app => { app.UseIdempotency(opts => opts.StrictPayload = true); app.Run(async ctx => { await ctx.Response.WriteAsync("ok"); }); }) .Build(); var client = server.GetTestClient(); var req1 = new HttpRequestMessage(HttpMethod.Post, "/") { Content = new StringContent("{\"a\":1}", Encoding.UTF8, "application/json") }; req1.Headers.Add("Idempotency-Key", key); var resp1 = await client.SendAsync(req1); Assert.Equal(HttpStatusCode.OK, resp1.StatusCode); var req2 = new HttpRequestMessage(HttpMethod.Post, "/") { Content = new StringContent("{\"a\":2}", Encoding.UTF8, "application/json") }; req2.Headers.Add("Idempotency-Key", key); var resp2 = await client.SendAsync(req2); Assert.Equal(HttpStatusCode.Conflict, resp2.StatusCode); } } ``` --- ## 7. Things to keep in mind | Concern | Guidance | |---------|----------| | **Cache size** | Idempotent responses can be large (e.g., file uploads). Consider limiting the size or storing only a reference (e.g., a blob key) for very big payloads. | | **TTL** | Choose a TTL that matches your business semantics. For “create order” you might keep it 24 h; for “payment” maybe only a few minutes. | | **Security** | The key is supplied by the client; never trust it for authentication. Combine it with user‑specific data if you need per‑user isolation. | | **Idempotent vs. safe** | This middleware only deduplicates *non‑GET* requests. GETs are already safe and cache‑friendly. | | **Streaming responses** | The current implementation buffers the whole response in memory. For very large streaming responses you would need a different strategy (e.g., write to a temporary file). | | **Distributed cache consistency** | When using Redis or SQL Server, the cache is shared across all instances, guaranteeing deduplication even in a scaled‑out environment. | --- ## 8. Full source tree (for copy‑paste) ``` /IdempotencyMiddleware │ IdempotentResponse.cs │ IdempotencyOptions.cs │ IdempotencyMiddleware.cs │ IdempotencyMiddlewareExtensions.cs │ (optional) IdempotencyMiddlewareTests.cs ``` You can drop the four `.cs` files into any ASP.NET Core project, register the middleware with `app.UseIdempotency(...)`, and you’re ready to safely deduplicate POST/PUT/PATCH/DELETE calls using an **Idempotency‑Key** header.

Enhanced response (with ZPI)

**Idempotency‑Key Middleware** The middleware below implements the classic *Idempotency‑Key* pattern for HTTP POST (and other non‑GET) requests. * A client sends a unique value in the `Idempotency-Key` header. * The middleware checks a distributed cache (e.g. Redis, SQL Server, NCache) for a stored entry with that key. * If an entry exists, the cached response (status code, headers, body) is written back to the client – the request is **deduplicated**. * If no entry exists, the request is allowed to continue down the pipeline. * The response is captured, cached for the configured TTL, and then written to the client. The implementation uses only built‑in ASP.NET Core abstractions (`IMiddleware`, `RequestDelegate`) and `IDistributedCache`. It is deliberately cache‑agnostic – you can plug any `IDistributedCache` implementation (Redis, SQL Server, NCache, MemoryDistributedCache, etc.) via DI. --- ## 1. Middleware implementation ```csharp using System; using System.IO; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; using System.Text.Json; namespace MyApp.Middleware; /// <summary> /// Middleware that enforces idempotency for non‑GET requests using the /// Idempotency-Key header. It stores the response in an <see cref="IDistributedCache"/> /// so that duplicate requests with the same key return the cached response /// instead of re‑executing the endpoint logic. /// </summary> public sealed class IdempotencyKeyMiddleware : IMiddleware { private const string HeaderName = "Idempotency-Key"; private readonly IDistributedCache _cache; private readonly ILogger<IdempotencyKeyMiddleware> _logger; private readonly TimeSpan _cacheEntryLifetime; /// <summary> /// Creates a new instance. /// </summary> /// <param name="cache">Distributed cache used to store responses.</param> /// <param name="logger">Logger for diagnostics.</param> /// <param name="cacheEntryLifetime"> /// How long a cached response is considered valid. Default: 24 h. /// </param> public IdempotencyKeyMiddleware( IDistributedCache cache, ILogger<IdempotencyKeyMiddleware> logger, TimeSpan? cacheEntryLifetime = null) { _cache = cache ?? throw new ArgumentNullException(nameof(cache)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _cacheEntryLifetime = cacheEntryLifetime ?? TimeSpan.FromHours(24); } public async Task InvokeAsync(HttpContext context, RequestDelegate next) { // Only apply to methods that can change state. if (!HttpMethods.IsPost(context.Request.Method) && !HttpMethods.IsPut(context.Request.Method) && !HttpMethods.IsPatch(context.Request.Method) && !HttpMethods.IsDelete(context.Request.Method)) { await next(context); return; } // The client must supply an Idempotency-Key header. if (!context.Request.Headers.TryGetValue(HeaderName, out var keyValues) || string.IsNullOrWhiteSpace(keyValues)) { _logger.LogWarning("Missing or empty {HeaderName} header on {Method} {Path}", HeaderName, context.Request.Method, context.Request.Path); context.Response.StatusCode = StatusCodes.Status400BadRequest; await context.Response.WriteAsync($"{HeaderName} header is required."); return; } var idempotencyKey = keyValues.ToString(); // Try to fetch a cached response. var cached = await _cache.GetAsync(idempotencyKey); if (cached != null) { _logger.LogInformation("Idempotent request hit cache for key {Key}", idempotencyKey); await WriteCachedResponseAsync(context, cached); return; } // No cached entry – capture the response for later caching. var originalBody = context.Response.Body; await using var memoryStream = new MemoryStream(); context.Response.Body = memoryStream; try { await next(context); } catch (Exception ex) { // If the downstream pipeline throws, we do NOT cache the response. _logger.LogError(ex, "Exception while processing request with Idempotency-Key {Key}", idempotencyKey); // Restore original body before re‑throwing. context.Response.Body = originalBody; throw; } // Ensure the response body is fully written. await context.Response.Body.FlushAsync(); // Read the captured response. memoryStream.Seek(0, SeekOrigin.Begin); var responseBody = await new StreamReader(memoryStream, Encoding.UTF8).ReadToEndAsync(); // Build a cache payload that contains status, headers and body. var payload = new CachedResponse { StatusCode = context.Response.StatusCode, Headers = context.Response.Headers, Body = responseBody }; var serialized = JsonSerializer.Serialize(payload); var bytes = Encoding.UTF8.GetBytes(serialized); // Store in cache. var options = new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = _cacheEntryLifetime }; await _cache.SetAsync(idempotencyKey, bytes, options); _logger.LogInformation("Cached response for Idempotency-Key {Key} (TTL {TTL})", idempotencyKey, _cacheEntryLifetime); // Reset the stream position and copy to the original response body. memoryStream.Seek(0, SeekOrigin.Begin); await memoryStream.CopyToAsync(originalBody); context.Response.Body = originalBody; } private static async Task WriteCachedResponseAsync(HttpContext context, byte[] cachedBytes) { var json = Encoding.UTF8.GetString(cachedBytes); var cached = JsonSerializer.Deserialize<CachedResponse>(json); if (cached == null) { // Corrupted cache entry – treat as a miss. context.Response.StatusCode = StatusCodes.Status500InternalServerError; await context.Response.WriteAsync("Corrupted idempotency cache entry."); return; } context.Response.StatusCode = cached.StatusCode; // Clear any existing headers that might have been set earlier. context.Response.Headers.Clear(); foreach (var header in cached.Headers) { // Some headers (e.g., Set-Cookie) can have multiple values. context.Response.Headers[header.Key] = header.Value; } // Write the stored body. await context.Response.WriteAsync(cached.Body); } // Helper DTO for serialization. private sealed class CachedResponse { public int StatusCode { get; set; } public IHeaderDictionary Headers { get; set; } = new HeaderDictionary(); public string Body { get; set; } = string.Empty; } } ``` ### How it works | Step | Description | |------|-------------| | **1. Method filter** | Only POST/PUT/PATCH/DELETE are considered; GET/HEAD are passed through unchanged. | | **2. Header validation** | Returns **400 Bad Request** if the `Idempotency-Key` header is missing or empty. | | **3. Cache lookup** | `IDistributedCache.GetAsync(key)` – if found, the stored response (status, headers, body) is written back and the request pipeline stops. | | **4. Capture response** | The original `HttpResponse.Body` is swapped with a `MemoryStream`. After the downstream endpoint finishes, the body is read, serialized together with status and headers, and stored in the cache. | | **5. Write response** | The captured stream is copied back to the original response stream so the client receives the normal response. | | **6. Error handling** | Exceptions from downstream are *not* cached; the original body stream is restored and the exception re‑thrown. Corrupted cache entries result in a **500** response. | --- ## 2. DI registration & pipeline integration ```csharp using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Caching.StackExchangeRedis; using Microsoft.Extensions.DependencyInjection; using MyApp.Middleware; var builder = WebApplication.CreateBuilder(args); // --------------------------------------------------------------------- // Distributed cache registration (example: Redis) // --------------------------------------------------------------------- builder.Services.AddStackExchangeRedisCache(options => { options.Configuration = builder.Configuration.GetConnectionString("Redis"); options.InstanceName = "IdempotencyDemo:"; }); // --------------------------------------------------------------------- // Register the middleware as a service (IMiddleware) // --------------------------------------------------------------------- builder.Services.AddTransient<IdempotencyKeyMiddleware>(); // --------------------------------------------------------------------- // Optional: expose configuration via IOptions if you want to make TTL configurable // --------------------------------------------------------------------- builder.Services.Configure<IdempotencyOptions>(builder.Configuration.GetSection("Idempotency")); // --------------------------------------------------------------------- // Build the app // --------------------------------------------------------------------- var app = builder.Build(); // --------------------------------------------------------------------- // Insert the middleware early in the pipeline (before MVC, before // authentication if you want the key to be evaluated even for unauthenticated // requests). Adjust ordering to your needs. // --------------------------------------------------------------------- app.UseMiddleware<IdempotencyKeyMiddleware>(); // --------------------------------------------------------------------- // Typical MVC / minimal API endpoints // --------------------------------------------------------------------- app.MapPost("/orders", async (HttpContext ctx) => { // Simulate work await Task.Delay(100); return Results.Created($"/orders/{Guid.NewGuid()}", new { Message = "Order created" }); }); app.Run(); ``` ### Configuration class (optional) ```csharp namespace MyApp.Middleware; /// <summary> /// Allows external configuration of the idempotency middleware (e.g. via appsettings.json). /// </summary> public sealed class IdempotencyOptions { /// <summary> /// How long a cached response lives. Default is 24 h. /// </summary> public TimeSpan CacheLifetime { get; set; } = TimeSpan.FromHours(24); } ``` If you want the middleware to read the TTL from `IOptions<IdempotencyOptions>`, inject it in the constructor and use `options.CacheLifetime`. --- ## 3. Usage notes & best practices * **Idempotency‑Key generation** – Clients should generate a UUID (or any globally unique value) *per logical operation* and reuse it for retries. * **Cache size** – Because each cached entry stores the full response body, monitor cache memory/size. For large payloads consider compressing the body before caching. * **Security** – The key is opaque; do **not** expose internal identifiers. Treat it as a secret – an attacker could replay a request if they guess a key. * **TTL** – Choose a TTL that matches the business semantics (e.g., 5 min for quick operations, 24 h for order creation). * **Idempotent GET** – GET is already safe; the middleware skips it. If you need to deduplicate other verbs, add them to the method filter. * **Distributed cache** – In a multi‑instance deployment you *must* use a distributed cache (Redis, SQL Server, NCache, etc.) so that all nodes share the same deduplication state. --- ### Summary The `IdempotencyKeyMiddleware` above gives you a drop‑in, production‑ready solution for deduplicating POST (and other state‑changing) requests using the `Idempotency-Key` header. It leverages ASP.NET Core’s `IMiddleware` abstraction, works with any `IDistributedCache` implementation, and includes thorough logging, error handling, and easy DI registration.

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-haiku-4-5-20251001
Better
V4qwen2.5-coder-7b-instruct
Better

Comments

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.

← back to all rounds