Round #348
08 May 2026, 17:08· csharp· claude-sonnet-4-6· r13-clean-hints
- Complexity
- band 0
- ZP / UO / OBS
- 4 / 0 / 0
- Journey weight
- 294
- Lightshift ΔL
- +0.38
Prompt
Implement idempotency key middleware that deduplicates POST requests
Zero-point index (what the engine surfaced)
ZPI-ZPI-0002 | 2026-05-08T17:08:19Z
Implement idempotency key middleware that deduplicates POST requests
ZERO POINT — 4 entries (structural invariants)
● hsl(314.84, 100%, 29%) IMiddleware [contract]
● hsl(280.69, 100%, 29%) RequestDelegate [contract]
● hsl(329.61, 100%, 18%) IDistributedCache [contract]
● hsl(136.10, 100%, 29%) 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 for ASP.NET Core
## Overview
This implementation provides a complete idempotency key middleware that:
- Intercepts POST requests with an `Idempotency-Key` header
- Caches responses for duplicate requests
- Returns cached responses without re-executing the handler
- Handles concurrent duplicate requests safely
- Supports configurable TTL and storage backends
## Implementation
### 1. Core Models
```csharp
// Models/IdempotencyModels.cs
using System.Net;
namespace IdempotencyMiddleware.Models;
public class CachedResponse
{
public int StatusCode { get; set; }
public Dictionary<string, string[]> Headers { get; set; } = new();
public byte[] Body { get; set; } = Array.Empty<byte>();
public string ContentType { get; set; } = string.Empty;
public DateTimeOffset CachedAt { get; set; }
public DateTimeOffset ExpiresAt { get; set; }
public string RequestFingerprint { get; set; } = string.Empty;
public bool IsComplete { get; set; }
}
public class IdempotencyOptions
{
public TimeSpan CacheDuration { get; set; } = TimeSpan.FromHours(24);
public TimeSpan LockTimeout { get; set; } = TimeSpan.FromSeconds(30);
public int MaxKeyLength { get; set; } = 255;
public bool EnforceRequestFingerprint { get; set; } = true;
public string HeaderName { get; set; } = "Idempotency-Key";
public List<string> ApplicableMethods { get; set; } = new() { "POST", "PATCH" };
public List<string> ExcludedPaths { get; set; } = new();
public Func<HttpContext, bool>? CustomFilter { get; set; }
}
public enum IdempotencyStatus
{
New,
Processing,
Completed,
Expired
}
```
### 2. Cache Store Interface & Implementations
```csharp
// Store/IIdempotencyStore.cs
namespace IdempotencyMiddleware.Store;
public interface IIdempotencyStore
{
Task<CachedResponse?> GetAsync(string key, CancellationToken ct = default);
Task<bool> SetAsync(string key, CachedResponse response, TimeSpan ttl, CancellationToken ct = default);
Task<bool> AcquireLockAsync(string key, TimeSpan timeout, CancellationToken ct = default);
Task ReleaseLockAsync(string key, CancellationToken ct = default);
Task<bool> ExistsAsync(string key, CancellationToken ct = default);
}
```
```csharp
// Store/InMemoryIdempotencyStore.cs
using System.Collections.Concurrent;
using IdempotencyMiddleware.Models;
namespace IdempotencyMiddleware.Store;
public class InMemoryIdempotencyStore : IIdempotencyStore, IDisposable
{
private readonly ConcurrentDictionary<string, (CachedResponse Response, DateTimeOffset Expires)> _cache = new();
private readonly ConcurrentDictionary<string, SemaphoreSlim> _locks = new();
private readonly Timer _cleanupTimer;
public InMemoryIdempotencyStore()
{
// Cleanup expired entries every 5 minutes
_cleanupTimer = new Timer(Cleanup, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
}
public Task<CachedResponse?> GetAsync(string key, CancellationToken ct = default)
{
if (_cache.TryGetValue(key, out var entry) && entry.Expires > DateTimeOffset.UtcNow)
return Task.FromResult<CachedResponse?>(entry.Response);
_cache.TryRemove(key, out _);
return Task.FromResult<CachedResponse?>(null);
}
public Task<bool> SetAsync(string key, CachedResponse response, TimeSpan ttl, CancellationToken ct = default)
{
var expires = DateTimeOffset.UtcNow.Add(ttl);
_cache[key] = (response, expires);
return Task.FromResult(true);
}
public async Task<bool> AcquireLockAsync(string key, TimeSpan timeout, CancellationToken ct = default)
{
var semaphore = _locks.GetOrAdd(key, _ => new SemaphoreSlim(1, 1));
return await semaphore.WaitAsync(timeout, ct);
}
public Task ReleaseLockAsync(string key, CancellationToken ct = default)
{
if (_locks.TryGetValue(key, out var semaphore))
semaphore.Release();
return Task.CompletedTask;
}
public Task<bool> ExistsAsync(string key, CancellationToken ct = default)
{
var exists = _cache.TryGetValue(key, out var entry) && entry.Expires > DateTimeOffset.UtcNow;
return Task.FromResult(exists);
}
private void Cleanup(object? state)
{
var now = DateTimeOffset.UtcNow;
foreach (var key in _cache.Keys)
{
if (_cache.TryGetValue(key, out var entry) && entry.Expires <= now)
_cache.TryRemove(key, out _);
}
}
public void Dispose()
{
_cleanupTimer.Dispose();
foreach (var semaphore in _locks.Values)
semaphore.Dispose();
}
}
```
```csharp
// Store/RedisIdempotencyStore.cs
using System.Text.Json;
using IdempotencyMiddleware.Models;
using StackExchange.Redis;
namespace IdempotencyMiddleware.Store;
public class RedisIdempotencyStore : IIdempotencyStore
{
private readonly IConnectionMultiplexer _redis;
private readonly IDatabase _db;
private const string LockPrefix = "idempotency:lock:";
private const string CachePrefix = "idempotency:cache:";
public RedisIdempotencyStore(IConnectionMultiplexer redis)
{
_redis = redis;
_db = redis.GetDatabase();
}
public async Task<CachedResponse?> GetAsync(string key, CancellationToken ct = default)
{
var value = await _db.StringGetAsync(CachePrefix + key);
return value.HasValue
? JsonSerializer.Deserialize<CachedResponse>(value!)
: null;
}
public async Task<bool> SetAsync(string key, CachedResponse response, TimeSpan ttl, CancellationToken ct = default)
{
var serialized = JsonSerializer.Serialize(response);
return await _db.StringSetAsync(CachePrefix + key, serialized, ttl);
}
public async Task<bool> AcquireLockAsync(string key, TimeSpan timeout, CancellationToken ct = default)
{
var lockKey = LockPrefix + key;
var deadline = DateTimeOffset.UtcNow.Add(timeout);
var retryDelay = TimeSpan.FromMilliseconds(50);
while (DateTimeOffset.UtcNow < deadline)
{
// SET NX EX — atomic lock acquisition
var acquired = await _db.StringSetAsync(
lockKey,
Environment.MachineName,
timeout,
When.NotExists
);
if (acquired) return true;
await Task.Delay(retryDelay, ct);
retryDelay = TimeSpan.FromMilliseconds(Math.Min(retryDelay.TotalMilliseconds * 2, 500));
}
return false;
}
public async Task ReleaseLockAsync(string key, CancellationToken ct = default)
{
Enhanced response (with ZPI)
# Idempotency Key Middleware
## Overview
This implementation intercepts POST requests, checks for an `Idempotency-Key` header, and returns cached responses for duplicate requests — preventing double-charges, duplicate orders, etc.
---
## NuGet Packages
```xml
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />
<PackageReference Include="System.Text.Json" Version="8.0.0" />
```
---
## 1. Models
```csharp
// Models/IdempotencyRecord.cs
using System.ComponentModel.DataAnnotations;
namespace IdempotencyMiddleware.Models;
public sealed class IdempotencyRecord
{
[Key]
public Guid Id { get; init; } = Guid.NewGuid();
[Required, MaxLength(256)]
public string IdempotencyKey { get; init; } = default!;
[Required, MaxLength(512)]
public string RequestPath { get; init; } = default!;
[Required, MaxLength(32)]
public string RequestMethod { get; init; } = default!;
/// <summary>MD5 hash of request body to detect key reuse with different payloads.</summary>
[MaxLength(64)]
public string? RequestBodyHash { get; init; }
public int ResponseStatusCode { get; set; }
public string? ResponseBody { get; set; }
public string? ResponseHeaders { get; set; }
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
public DateTimeOffset ExpiresAt { get; init; }
public IdempotencyStatus Status { get; set; } = IdempotencyStatus.Processing;
}
public enum IdempotencyStatus
{
Processing = 0,
Completed = 1,
Failed = 2
}
```
```csharp
// Models/CachedResponse.cs
namespace IdempotencyMiddleware.Models;
public sealed record CachedResponse(
int StatusCode,
string Body,
Dictionary<string, string[]> Headers
);
```
---
## 2. Database Context
```csharp
// Data/IdempotencyDbContext.cs
using IdempotencyMiddleware.Models;
using Microsoft.EntityFrameworkCore;
namespace IdempotencyMiddleware.Data;
public sealed class IdempotencyDbContext(DbContextOptions<IdempotencyDbContext> options)
: DbContext(options)
{
public DbSet<IdempotencyRecord> IdempotencyRecords => Set<IdempotencyRecord>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<IdempotencyRecord>(entity =>
{
entity.HasIndex(e => e.IdempotencyKey)
.IsUnique()
.HasDatabaseName("IX_IdempotencyRecords_Key");
entity.HasIndex(e => e.ExpiresAt)
.HasDatabaseName("IX_IdempotencyRecords_ExpiresAt");
entity.Property(e => e.Status)
.HasConversion<string>();
});
}
}
```
---
## 3. Options
```csharp
// Options/IdempotencyOptions.cs
namespace IdempotencyMiddleware.Options;
public sealed class IdempotencyOptions
{
public const string SectionName = "Idempotency";
/// <summary>How long idempotency keys are retained.</summary>
public TimeSpan KeyExpiry { get; set; } = TimeSpan.FromHours(24);
/// <summary>Maximum wait time for an in-flight (Processing) request to complete.</summary>
public TimeSpan LockTimeout { get; set; } = TimeSpan.FromSeconds(30);
/// <summary>Polling interval when waiting for an in-flight request.</summary>
public TimeSpan LockPollingInterval { get; set; } = TimeSpan.FromMilliseconds(250);
/// <summary>HTTP methods subject to idempotency checks.</summary>
public HashSet<string> ApplicableMethods { get; set; } = new(StringComparer.OrdinalIgnoreCase)
{
HttpMethods.Post
};
/// <summary>Route prefixes to exclude (e.g. health checks, auth).</summary>
public HashSet<string> ExcludedPaths { get; set; } = new(StringComparer.OrdinalIgnoreCase)
{
"/health",
"/metrics"
};
/// <summary>Response headers that should NOT be replayed.</summary>
public HashSet<string> ExcludedReplayHeaders { get; set; } = new(StringComparer.OrdinalIgnoreCase)
{
"Transfer-Encoding",
"Content-Length",
"Date",
"Set-Cookie"
};
/// <summary>Require the header — reject requests that omit it.</summary>
public bool EnforceIdempotencyKey { get; set; } = false;
/// <summary>Cache layer: Redis preferred, falls back to in-memory.</summary>
public bool UseDistributedCache { get; set; } = true;
}
```
---
## 4. Idempotency Store (abstraction + two implementations)
```csharp
// Store/IIdempotencyStore.cs
using IdempotencyMiddleware.Models;
namespace IdempotencyMiddleware.Store;
public interface IIdempotencyStore
{
/// <summary>
/// Atomically inserts a Processing record.
/// Returns (true, null) → caller owns the request, must call Complete/Fail.
/// Returns (false, record) → a prior record exists; caller should replay it.
/// </summary>
Task<(bool IsOwner, IdempotencyRecord? ExistingRecord)> TryAcquireAsync(
string key, string path, string method, string? bodyHash,
TimeSpan expiry, CancellationToken ct = default);
Task<IdempotencyRecord?> GetAsync(string key, CancellationToken ct = default);
Task CompleteAsync(string key, CachedResponse response, CancellationToken ct = default);
Task FailAsync(string key, CancellationToken ct = default);
}
```
```csharp
// Store/DistributedCacheIdempotencyStore.cs
using System.Text.Json;
using IdempotencyMiddleware.Models;
using Microsoft.Extensions.Caching.Distributed;
namespace IdempotencyMiddleware.Store;
/// <summary>Redis-backed store. Uses a "lock" key to achieve atomic acquire semantics.</summary>
public sealed class DistributedCacheIdempotencyStore(IDistributedCache cache) : IIdempotencyStore
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
private static string LockKey(string key) => $"idempotency:lock:{key}";
private static string RecordKey(string key) => $"idempotency:record:{key}";
public async Task<(bool IsOwner, IdempotencyRecord? ExistingRecord)> TryAcquireAsync(
string key, string path, string method, string? bodyHash,
TimeSpan expiry, CancellationToken ct = default)
{
// Check for existing completed/in-flight record first
var existing = await GetAsync(key, ct);
if (existing is not null)
return (false, existing);
// Try to set a distributed lock (NX semantics via SetAsync with condition)
// Redis does not expose NX directly through IDistributedCache, so we use
// a short absolute-expiry "lock" key as a best-effort compare-and-set.
var lockEntry = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.
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
BetterV4qwen2.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.