Idempotency¶
The Idempotency module provides at-most-once execution guarantees for HTTP requests, webhook deliveries, and event processing. It uses a key-based claim mechanism with optional response caching, configurable via IdempotencyOptions.
Quick Start¶
using Deepstaging.Idempotency;
// In Program.cs — add middleware
app.UseIdempotency();
// Configuration (appsettings.json)
{
"Deepstaging": {
"Idempotency": {
"HeaderName": "Idempotency-Key",
"DefaultTtl": "1.00:00:00",
"RequiredMethods": ["POST", "PUT", "PATCH"],
"ReturnCachedResponse": true
}
}
}
Clients include an Idempotency-Key header; the middleware deduplicates automatically:
Features¶
| Feature | Description |
|---|---|
IIdempotencyStore |
Core abstraction — TryClaimAsync, IsClaimedAsync, ReleaseAsync, response caching |
IdempotencyMiddleware |
ASP.NET Core middleware that intercepts requests by header key |
IdempotencyOptions |
[ConfigSection] — header name, TTL, required methods, excluded paths, response caching mode |
| Response caching | TryGetCachedResponseAsync / StoreCachedResponseAsync — replay identical responses for duplicate keys |
CachedResponse |
Captures status code, headers, and body for replay |
| Effects module | IdempotencyModule for Eff<RT, T> composition |
| Test double | TestIdempotencyStore with call recording |
| InMemory | InMemoryIdempotencyStore with concurrent dictionary + TTL expiry |
Core Interface¶
public interface IIdempotencyStore
{
Task<bool> TryClaimAsync(string key, TimeSpan? expiry = null, CancellationToken ct = default);
Task<bool> IsClaimedAsync(string key, CancellationToken ct = default);
Task ReleaseAsync(string key, CancellationToken ct = default);
Task<CachedResponse?> TryGetCachedResponseAsync(string key, CancellationToken ct = default);
Task StoreCachedResponseAsync(string key, CachedResponse response, TimeSpan? expiry = null, CancellationToken ct = default);
}
Configuration¶
| Property | Default | Description |
|---|---|---|
HeaderName |
"Idempotency-Key" |
HTTP header to read the key from |
DefaultTtl |
24 hours | How long claimed keys persist |
RequiredMethods |
POST, PUT, PATCH |
HTTP methods that require an idempotency key |
ExcludedPaths |
/health, /swagger |
Paths that bypass idempotency checks |
ReturnCachedResponse |
true |
Return cached response for duplicates (true) or 409 Conflict (false) |
How It Works¶
- Client sends request with
Idempotency-Keyheader - Middleware calls
TryClaimAsync(key)onIIdempotencyStore - First request: claim succeeds → request proceeds → response is cached via
StoreCachedResponseAsync - Duplicate request: claim fails → cached response is returned (or 409 if
ReturnCachedResponse = false) - Keys expire after
DefaultTtl
Dispatch Attribute¶
Mark handlers as idempotent with [Idempotent]:
[CommandHandler]
[HttpPost("/orders")]
[Idempotent]
public static Eff<AppRuntime, OrderPlaced> Handle(CreateOrder cmd) => ...
See Attributes for details.
Effects Composition¶
Use IdempotencyModule for manual idempotency in event handlers and background jobs:
from claimed in IdempotencyModule.Idempotency.TryClaimAsync<AppRuntime>(
$"webhook:{eventId}", TimeSpan.FromHours(24))
from _ in claimed ? ProcessEvent(event) : unitEff
select unit;
See Effects Composition for multi-step pipelines.
Sub-Pages¶
| Page | Description |
|---|---|
| Attributes | [Idempotent] attribute and configuration options |
| Effects Composition | Manual Eff<RT, T> idempotency — claim, release, recovery |
| Generated Code | What [EffectsModule] emits — capability interface, effect methods |
| Testing | TestIdempotencyStore — seeding claimed keys, asserting claims |
Source¶
src/Core/Deepstaging.Runtime/Idempotency/