Idempotent.NET
1.0.0
dotnet add package Idempotent.NET --version 1.0.0
NuGet\Install-Package Idempotent.NET -Version 1.0.0
<PackageReference Include="Idempotent.NET" Version="1.0.0" />
<PackageVersion Include="Idempotent.NET" Version="1.0.0" />
<PackageReference Include="Idempotent.NET" />
paket add Idempotent.NET --version 1.0.0
#r "nuget: Idempotent.NET, 1.0.0"
#:package Idempotent.NET@1.0.0
#addin nuget:?package=Idempotent.NET&version=1.0.0
#tool nuget:?package=Idempotent.NET&version=1.0.0

Idempotent.NET
Idempotent.NET gives ASP.NET Core APIs a Stripe-style idempotency layer: a client sends one Idempotency-Key, the first request executes, and safe retries receive the stored result instead of running the side effect again.
It is built for the messy part of real systems: mobile retries, load balancers, client timeouts, queue redelivery, double-clicks, and services that must not create the same payment, order, booking, or command twice.
Install
dotnet add package Idempotent.NET
Add a durable store when the app runs on more than one node:
dotnet add package Idempotent.NET.Stores.Redis
dotnet add package Idempotent.NET.Stores.Sql
dotnet add package Idempotent.NET.Stores.EntityFrameworkCore
Minimal API
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddIdempotency()
.AddMemoryStore();
var app = builder.Build();
app.UseIdempotency();
app.MapPost("/orders", (CreateOrder request) =>
Results.Created($"/orders/{request.Id}", request))
.RequireIdempotency();
app.Run();
Send an Idempotency-Key header with unsafe requests:
POST /orders HTTP/1.1
Idempotency-Key: 7f36df0d-a8e8-4b9f-a673-cbc90f11b9f4
Content-Type: application/json
{ "sku": "coffee", "quantity": 2 }
The first request claims the key and stores the status code, headers, body, and fingerprint. A retry with the same key and same request fingerprint gets the cached response. A retry with the same key but different request content is rejected as a conflict.
Controllers
[ApiController]
[Route("orders")]
public sealed class OrdersController : ControllerBase
{
[HttpPost]
[Idempotent]
public IActionResult Create(CreateOrder request) =>
Created($"/orders/{request.Id}", request);
}
Register the middleware once:
builder.Services.AddIdempotency().AddMemoryStore();
app.UseIdempotency();
Stores
| Store | Package | Use it for |
|---|---|---|
| Memory | Idempotent.NET / Idempotent.NET.Stores.Memory |
Local development, tests, single-node services |
| Redis | Idempotent.NET.Stores.Redis |
Low-latency shared state across many API nodes |
| SQL | Idempotent.NET.Stores.Sql |
PostgreSQL, SQL Server, or SQLite without EF Core |
| EF Core | Idempotent.NET.Stores.EntityFrameworkCore |
Apps that want idempotency rows inside an existing DbContext |
Redis
builder.Services
.AddIdempotency()
.AddRedisStore(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis")!;
options.KeyPrefix = "orders-api:";
});
SQL
Supports PostgreSQL, SQL Server, and SQLite. The store auto-creates the idempotency_keys table on first startup (AutoMigrate = true by default) — no manual DDL or migration script needed.
builder.Services
.AddIdempotency()
.AddSqlStore(options =>
{
options.Provider = SqlProvider.PostgreSql; // PostgreSql | SqlServer | Sqlite
options.ConnectionString = builder.Configuration.GetConnectionString("Postgres")!;
// options.AutoMigrate = false; // opt out if you manage schema yourself
// options.TableName = "idempotency_keys"; // override table name
});
<details> <summary>Table schema (all providers)</summary>
| Column | PostgreSQL | SQL Server | SQLite |
|---|---|---|---|
storage_key (PK) |
VARCHAR(64) |
NVARCHAR(64) |
TEXT |
state |
INTEGER |
INTEGER |
INTEGER |
fingerprint_hex |
VARCHAR(64) |
NVARCHAR(64) |
TEXT |
created_at_ticks |
BIGINT |
BIGINT |
INTEGER |
completed_at_ticks |
BIGINT |
BIGINT |
INTEGER |
lease_expires_at_ticks |
BIGINT |
BIGINT |
INTEGER |
expires_at_ticks |
BIGINT |
BIGINT |
INTEGER |
status_code |
INTEGER |
INTEGER |
INTEGER |
headers_json |
TEXT |
NVARCHAR(MAX) |
TEXT |
body_base64 |
TEXT |
NVARCHAR(MAX) |
TEXT |
content_type |
TEXT |
NVARCHAR(MAX) |
TEXT |
body_truncated |
INTEGER |
INTEGER |
INTEGER |
Storage keys are SHA-256 hashes — the raw client Idempotency-Key value never appears in the database.
</details>
EF Core
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.AddIdempotencyKeys();
// optionally: modelBuilder.Entity<IdempotencyKeyEntity>().ToTable("my_keys");
}
How It Works
new key
-> claim as InFlight
-> execute handler
-> store response + fingerprint + TTL
-> replay later identical requests
duplicate in-flight key
-> return conflict or wait, based on options
duplicate completed key with a different fingerprint
-> reject as fingerprint mismatch
Storage keys are SHA-256 hashes of the client key, scope, HTTP method, and path. The raw client key is never interpolated into SQL and is not used directly as a Redis key.
Beyond HTTP
MediatR pipeline behavior
builder.Services.AddIdempotentBehavior();
Implement IIdempotentRequest<TResponse> on any MediatR request to make it idempotent end-to-end, including through message queues and retry loops.
Outbound HTTP key injection
builder.Services.AddHttpClient("payments")
.AddIdempotencyKeyHandler();
Propagates the current request's idempotency key to outbound HttpClient calls automatically, so downstream services deduplicate on the same key without extra code.
Wolverine inbox pattern
See samples/InboxWithMassTransit/ for an end-to-end inbox example using Wolverine.
Source Generator and Analyzers
The Idempotent.NET.SourceGenerators package (automatically included with Idempotent.NET) ships three Roslyn analyzers and a compile-time registry generator.
Analyzers
| Diagnostic | Severity | Description |
|---|---|---|
| IDM001 | Warning | [Idempotent] applied to a safe HTTP verb (GET, HEAD, OPTIONS). Safe verbs are already idempotent; the attribute has no effect and signals a design mistake. |
| IDM002 | Info | [Idempotent(RequireKey = true)] is declared but no 400 response is documented in OpenAPI attributes. Clients need to know a missing key returns 400. |
| IDM003 | Error | A type implements IIdempotentRequest<T> but has no IdempotencyKey string property. The MediatR behavior requires this property to compute the storage key. |
Registry generator
At compile time the generator discovers every IIdempotentRequest<T> implementation in your assembly and emits:
// auto-generated — do not edit
public static partial class IdempotencyEndpointRegistry
{
public static readonly FrozenSet<string> RequestTypes = FrozenSet.ToFrozenSet(new[]
{
"MyApp.Commands.CreateOrderCommand",
// ...
});
}
This lets tooling and middleware inspect registered idempotent request types at zero runtime cost.
vs IdempotentAPI
IdempotentAPI is the established library in this space. Here is a direct comparison:
| Feature | Idempotent.NET | IdempotentAPI v2.6 |
|---|---|---|
Minimal API (RequireIdempotency()) |
✅ First-class | ❌ Controllers only |
| MediatR pipeline behavior | ✅ Built-in | ❌ Not supported |
Outbound HttpClient key injection |
✅ Built-in | ❌ Not supported |
| Wolverine inbox pattern | ✅ Sample included | ❌ Not supported |
| Raw SQL store (no ORM) | ✅ PostgreSQL, SQL Server, SQLite | ❌ Not supported |
| EF Core store | ✅ Plugs into existing DbContext |
✅ Supported |
| Redis store | ✅ Supported | ✅ Supported |
| Auto-creates SQL table | ✅ Yes (AutoMigrate = true) |
N/A |
| Roslyn analyzers | ✅ IDM001, IDM002, IDM003 | ❌ Not supported |
| .NET 10 support | ✅ | ❌ (as of v2.6) |
| Request fingerprinting (conflict detection) | ✅ SHA-256 | ✅ |
| In-flight lease / watchdog renewal | ✅ | ❌ |
The core difference in scope: IdempotentAPI is an HTTP filter for controller APIs. Idempotent.NET covers the full lifecycle — HTTP layer, message bus, outbound clients, and raw query infrastructure.
Benchmarks
BenchmarkDotNet ShortRun on Windows 11, .NET SDK 10.0.103, runtime .NET 10.0.7, x64 RyuJIT AVX2.
| Method | Mean | Allocated |
|---|---|---|
ComputeStorageKey |
436.7 ns | 600 B |
TryClaim_NewKey |
2,563.4 ns | 1008 B |
TryClaim_AlreadyCompleted |
580.4 ns | 752 B |
TryClaim_AlreadyInFlight |
627.5 ns | 752 B |
GetAsync_Completed |
492.1 ns | 736 B |
GetAsync_Missing |
440.1 ns | 592 B |
Run them locally:
dotnet run -c Release --project benchmarks/Idempotent.NET.Benchmarks
Packages
| Package | Purpose |
|---|---|
Idempotent.NET |
Main package with ASP.NET Core integration and memory store |
Idempotent.NET.Core |
Abstractions, keys, fingerprints, responses, and store contracts |
Idempotent.NET.AspNetCore |
Middleware, endpoint filters, and [Idempotent] |
Idempotent.NET.Stores.Memory |
In-process store |
Idempotent.NET.Stores.Redis |
Redis-backed distributed store |
Idempotent.NET.Stores.Sql |
Raw ADO.NET store for PostgreSQL, SQL Server, and SQLite |
Idempotent.NET.Stores.EntityFrameworkCore |
EF Core-backed store |
Idempotent.NET.Mediator |
MediatR request pipeline behavior |
Idempotent.NET.Http |
Outbound HttpClient idempotency-key handler |
Idempotent.NET.SourceGenerators |
Roslyn analyzers (IDM001–IDM003) and compile-time request registry |
Samples
The samples/ folder includes runnable Minimal API, controller, MediatR, Wolverine, outbound HTTP, and inbox examples.
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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 is compatible. 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. |
-
net10.0
- Idempotent.NET.AspNetCore (>= 1.0.0)
- Idempotent.NET.Core (>= 1.0.0)
- Idempotent.NET.Stores.Memory (>= 1.0.0)
-
net8.0
- Idempotent.NET.AspNetCore (>= 1.0.0)
- Idempotent.NET.Core (>= 1.0.0)
- Idempotent.NET.Stores.Memory (>= 1.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 97 | 5/3/2026 |
| 1.0.0-preview.2 | 53 | 5/3/2026 |
| 1.0.0-preview.1 | 57 | 5/3/2026 |
Initial public release of Idempotent.NET: ASP.NET Core idempotency middleware, in-memory/Redis/SQL/EF Core stores, MediatR-style pipeline behavior, outbound HttpClient key handling, source generator support, samples, tests, and benchmarks.