Idempo 0.1.0

dotnet add package Idempo --version 0.1.0
                    
NuGet\Install-Package Idempo -Version 0.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Idempo" Version="0.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Idempo" Version="0.1.0" />
                    
Directory.Packages.props
<PackageReference Include="Idempo" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Idempo --version 0.1.0
                    
#r "nuget: Idempo, 0.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Idempo@0.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Idempo&version=0.1.0
                    
Install as a Cake Addin
#tool nuget:?package=Idempo&version=0.1.0
                    
Install as a Cake Tool

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

  1. Request arrives for a configured method (POST/PATCH by default).
  2. Middleware reads the Idempotency-Key header.
  3. Computes StorageKey = hex(SHA-256(method + ":" + routePath + ":" + tenant? + ":" + clientKey)).
  4. TryAcquire against 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.
  5. Adds Idempo-Result: executing or Idempo-Result: replayed header 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-Key can 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 raw Idempotency-Key header value is never logged — only the StorageKey hash.
  • Distributed tracing: ActivitySource named "Idempo" with spans idempotency.check, idempotency.acquire, idempotency.replay and tag idempo.result.

License

MIT

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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