Idempo 0.1.0
dotnet add package Idempo --version 0.1.0
NuGet\Install-Package Idempo -Version 0.1.0
<PackageReference Include="Idempo" Version="0.1.0" />
<PackageVersion Include="Idempo" Version="0.1.0" />
<PackageReference Include="Idempo" />
paket add Idempo --version 0.1.0
#r "nuget: Idempo, 0.1.0"
#:package Idempo@0.1.0
#addin nuget:?package=Idempo&version=0.1.0
#tool nuget:?package=Idempo&version=0.1.0
Idempo
ASP.NET Core middleware for HTTP Idempotency-Key support — Stripe-style. Prevents duplicate processing of POST/PATCH requests when clients retry after network failures.
Packages
| Package | Description |
|---|---|
Idempo |
Core middleware + in-memory store. Zero third-party runtime dependencies. |
Idempo.Redis |
Distributed Redis store using StackExchange.Redis. |
Quick start — Controllers
// Program.cs
builder.Services.AddIdempotency(options =>
{
options.RequireIdempotencyKey = false; // global default
options.Expiration = TimeSpan.FromHours(24);
})
.AddIdempotencyInMemoryStore(); // single-node; use .AddIdempotencyRedisStore() in production
// ...
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseIdempotency(); // after UseRouting, before endpoints
app.MapControllers();
// PaymentsController.cs
[HttpPost]
[Idempotent(Required = true)] // 400 if Idempotency-Key header is absent
public async Task<IActionResult> CreatePayment([FromBody] CreatePaymentRequest request)
{
// ...
}
Clients include the header on every request:
POST /payments
Idempotency-Key: 7f3d4e2a-1b9c-4f5e-8a7b-3c2d1e0f9a8b
Content-Type: application/json
{"amount": 100, "currency": "USD"}
Retries with the same key and body receive the stored response. A different body returns 422 Unprocessable Entity.
Quick start — Minimal API
app.MapPost("/orders", CreateOrder)
.WithIdempotency(required: true);
Using Redis (distributed / multi-node)
builder.Services.AddIdempotency()
.AddIdempotencyRedisStore(options =>
{
options.Configuration = "redis-host:6379";
options.KeyPrefix = "myapp:idempo:";
});
// Or supply an existing IConnectionMultiplexer:
builder.Services.AddIdempotency()
.AddIdempotencyRedisStore(options =>
options.ConnectionMultiplexer = existingMultiplexer);
How it works
- Request arrives for a configured method (POST/PATCH by default).
- Middleware reads the
Idempotency-Keyheader. - Computes
StorageKey = hex(SHA-256(method + ":" + routePath + ":" + tenant? + ":" + clientKey)). TryAcquireagainst the store (atomic write-if-absent).- Acquired — wraps the response stream, calls the handler, stores the response on success.
- Existing (InProgress) — polls until the original request completes (up to
InProgressTimeout), then replays. - Existing (Completed) — validates body hash, replays stored response immediately.
- Adds
Idempo-Result: executingorIdempo-Result: replayedheader to every idempotency-handled response.
Cacheable vs non-cacheable status codes
Only 2xx, 400, 404, 409, 410, and 422 responses are stored by default (configured via CacheableStatusCodes). 5xx responses are never stored — the key is released so the next retry re-executes the handler.
FailOpen vs FailClosed
| Policy | Behaviour when store is unavailable |
|---|---|
FailOpen (default) |
Log a warning and pass through. Idempotency protection is silently skipped. |
FailClosed |
Return 503 Service Unavailable with Retry-After: 5. |
Use FailClosed for payment and financial flows where processing a charge twice is unacceptable:
builder.Services.AddIdempotency(options =>
{
options.StoreFailurePolicy = StoreFailureBehavior.FailClosed;
});
In-memory store warning
The built-in InMemoryIdempotencyStore stores all state in the current process's memory.
- Single-node deployments only. In a load-balanced or multi-instance deployment, two requests with the same
Idempotency-Keycan land on different nodes and both execute. - Not persistent. State is lost on restart; duplicate protection does not survive a process recycle.
For production multi-instance deployments, use Idempo.Redis.
Options reference
builder.Services.AddIdempotency(options =>
{
options.HeaderName = "Idempotency-Key"; // default
options.Methods = new HashSet<string> { "POST", "PATCH" }; // default
options.Expiration = TimeSpan.FromHours(24); // default
options.RequireIdempotencyKey = false; // default
options.ValidateBodyHash = true; // default; 422 on body mismatch
options.MaxBodyHashSizeBytes = 262_144; // 256 KiB default
options.LargeBodyPolicy = LargeBodyBehavior.SkipHash; // or Reject
options.CacheableStatusCodes = new HashSet<int> { 200, 201, 202, 204, 400, 404, 409, 410, 422 };
options.StoreFailurePolicy = StoreFailureBehavior.FailOpen; // or FailClosed
options.InProgressTimeout = TimeSpan.FromSeconds(10); // zombie protection
options.InProgressPollInterval = TimeSpan.FromMilliseconds(200);
options.TenantKeyExtractor = ctx => ctx.User.FindFirstValue("tid"); // optional: scope per tenant
});
Observability
- Logging: structured via
ILogger<IdempotencyMiddleware>. The rawIdempotency-Keyheader value is never logged — only theStorageKeyhash. - Distributed tracing:
ActivitySourcenamed"Idempo"with spansidempotency.check,idempotency.acquire,idempotency.replayand tagidempo.result.
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net6.0
- No dependencies.
-
net8.0
- No dependencies.
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Idempo:
| Package | Downloads |
|---|---|
|
Idempo.Redis
Redis-backed idempotency store for Idempo. Uses StackExchange.Redis with atomic Lua scripts for race-free duplicate detection across distributed deployments. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.1.0 | 107 | 6/3/2026 |