Idempotency.AspNetCore
0.0.1
dotnet add package Idempotency.AspNetCore --version 0.0.1
NuGet\Install-Package Idempotency.AspNetCore -Version 0.0.1
<PackageReference Include="Idempotency.AspNetCore" Version="0.0.1" />
<PackageVersion Include="Idempotency.AspNetCore" Version="0.0.1" />
<PackageReference Include="Idempotency.AspNetCore" />
paket add Idempotency.AspNetCore --version 0.0.1
#r "nuget: Idempotency.AspNetCore, 0.0.1"
#:package Idempotency.AspNetCore@0.0.1
#addin nuget:?package=Idempotency.AspNetCore&version=0.0.1
#tool nuget:?package=Idempotency.AspNetCore&version=0.0.1
Idempotency
A flexible and extensible idempotency library for ASP.NET Core applications that helps prevent duplicate request processing by tracking and managing request uniqueness.
Features
- 🔒 Prevent Duplicate Processing - Automatically detect and handle duplicate requests
- 🎯 Flexible Actor Identification - Support for authenticated users, anonymous users, or custom actor resolution
- 💾 Multiple Storage Options - In-memory store for development, MongoDB for production, or implement your own
- 🔑 Request Fingerprinting - Detect conflicting requests with different payloads using the same idempotency key
- ⚙️ Highly Configurable - Customize headers, status codes, response storage, and more
- 🚀 Easy Integration - Simple middleware-based setup for ASP.NET Core
Installation
# Core package for ASP.NET Core
dotnet add package Idempotency.AspNetCore
# In-memory store (included in Core)
# No additional package needed
# MongoDB store (for production)
dotnet add package Idempotency.Store.MongoDb
Quick Start
Basic Setup with In-Memory Store
using Idempotency.AspNetCore;
using Idempotency.AspNetCore.Extensions;
var builder = WebApplication.CreateBuilder(args);
// Add idempotency services with in-memory store
builder.Services
.AddIdempotency()
.UseInMemoryIdempotencyStore();
var app = builder.Build();
app.UseRouting();
// Add idempotency middleware (must be after UseRouting)
app.UseIdempotency();
// Mark endpoints as idempotent
app.MapPost("/orders", (Order order) =>
{
// Your order processing logic
return Results.Created($"/orders/{order.Id}", order);
})
.WithIdempotency();
app.Run();
Getting Started with ASP.NET Core
1. Configure Services
using Idempotency.AspNetCore;
using Idempotency.AspNetCore.ActorIds;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddIdempotency(options =>
{
// Customize the idempotency key header name (default: "X-Idempotency-Key")
options.HeaderName = "X-Idempotency-Key";
// Customize conflict status code (default: 409 Conflict)
options.ConflictStatusCode = StatusCodes.Status409Conflict;
// Customize conflict message
options.ConflictMessage = "Request is already in progress or conflicts with previous payload.";
// Define which responses should be stored (default: 2xx status codes)
options.ShouldStoreResponse = status => status is >= 200 and < 300;
// Specify which headers to store with the response
options.HeadersToStore = new[] { "Cache-Control", "Content-Encoding" };
})
.UseInMemoryIdempotencyStore(); // or UseMongoIdempotencyStore()
// Configure actor ID resolution (who is making the request)
// Option 1: Anonymous users (all requests treated as same actor)
builder.Services.AddSingleton<IActorIdFactory, AnonActorIdFactory>();
// Option 2: Authenticated users (default - uses ClaimTypes.NameIdentifier)
builder.Services.AddSingleton<IActorIdFactory>(
new ClaimActorIdFactory(ClaimTypes.NameIdentifier));
// Option 3: Custom claim type
builder.Services.AddSingleton<IActorIdFactory>(
new ClaimActorIdFactory("sub")); // or any custom claim type
2. Add Middleware
var app = builder.Build();
// Add idempotency middleware (must be after UseRouting)
app.UseRouting();
app.UseIdempotency();
app.MapControllers();
app.Run();
3. Mark Endpoints as Idempotent
Minimal APIs
// With automatic scope (uses request path)
app.MapPost("/payments", async (Payment payment) =>
{
// Process payment
return Results.Ok(new { transactionId = Guid.NewGuid() });
})
.WithIdempotency();
// With custom scope
app.MapPost("/orders", async (Order order) =>
{
// Process order
return Results.Created($"/orders/{order.Id}", order);
})
.WithIdempotency("orders");
MVC Controllers
using Idempotency.AspNetCore;
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
// With automatic scope (uses request path)
[HttpPost]
[Idempotent]
public IActionResult CreateOrder([FromBody] Order order)
{
// Process order
return CreatedAtAction(nameof(GetOrder), new { id = order.Id }, order);
}
// With custom scope
[HttpPut("{id}")]
[Idempotent(Scope = "orders")]
public IActionResult UpdateOrder(string id, [FromBody] Order order)
{
// Update order
return Ok(order);
}
[HttpGet("{id}")]
public IActionResult GetOrder(string id)
{
// Not idempotent - no attribute needed
return Ok(new Order());
}
}
Storage Options
In-Memory Store
builder.Services
.AddIdempotency()
.UseInMemoryIdempotencyStore();
MongoDB Store
using Idempotency.Store.MongoDb;
using MongoDB.Driver;
var mongoClient = new MongoClient("mongodb://localhost:27017");
var database = mongoClient.GetDatabase("MyAppDatabase");
builder.Services
.AddIdempotency()
.UseMongoIdempotencyStore(
database,
collectionName: "idempotency_records", // optional, default shown
configure: options =>
{
// How long to keep idempotency records (default: 30 minutes)
options.TimeToLive = TimeSpan.FromHours(24);
});
MongoDB Configuration:
- Records are automatically indexed with TTL
- Old records are automatically cleaned up based on
TimeToLivesetting - Collection is created automatically if it doesn't exist
How It Works
- Client sends request with
X-Idempotency-Keyheader - Middleware intercepts requests to endpoints marked with
.WithIdempotency() - Actor identification resolves who is making the request (user ID, anonymous, etc.)
- Claim attempt tries to claim ownership of the idempotency key
- Fingerprint check compares request payload hash with stored fingerprint
- Decision made:
- First request: Process normally and store response
- Duplicate request: Return stored response immediately
- Conflict: Same key, different payload → return 409 Conflict
- In progress: Another identical request is being processed → return 409 Conflict
- Response stored (if successful and matches storage criteria)
- Claim released (if request processing failed)
Idempotency Strategy
This library implements an at-most-once processing guarantee:
- Successful requests are stored and replayed on subsequent requests with the same idempotency key
- Failed requests release their claim, allowing retries
- No automatic retries - The library does not retry failed requests
Retry Handling
If you need retry logic for failed requests, combine this library with a resilience library like Polly
Important Notes:
- Each retry attempt should use the same idempotency key to ensure at-most-once processing
- The library guarantees that even with retries, the operation will only be processed once successfully
- If a request fails and the claim is released, a retry with the same key will attempt processing again
- Once a request succeeds, all subsequent requests with the same key return the cached response
Advanced Configuration
Custom Actor ID Resolution
using Idempotency.AspNetCore.ActorIds;
using Microsoft.AspNetCore.Http;
public class CustomActorIdFactory : IActorIdFactory
{
public Task<string?> ResolveActorId(HttpContext context)
{
// Example: Use API key from header
var apiKey = context.Request.Headers["X-API-Key"].FirstOrDefault();
return Task.FromResult(apiKey);
// Example: Use combination of claims
// var userId = context.User.FindFirstValue("user_id");
// var tenantId = context.User.FindFirstValue("tenant_id");
// return Task.FromResult($"{tenantId}:{userId}");
}
}
builder.Services.AddSingleton<IActorIdFactory, CustomActorIdFactory>();
Custom Scope Resolution
using Idempotency.AspNetCore.Scopes;
using Microsoft.AspNetCore.Http;
public class CustomScopeFactory : IScopeFactory
{
public Task<string?> ResolveScope(HttpContext context)
{
// Example: Use route pattern
var endpoint = context.GetEndpoint();
var routePattern = endpoint?.Metadata
.GetMetadata<RouteEndpointMetadata>()?.RoutePattern;
return Task.FromResult(routePattern);
}
}
builder.Services.AddSingleton<IScopeFactory, CustomScopeFactory>();
Custom Request Fingerprinting
using Idempotency.AspNetCore.Fingerprints;
using Idempotency.Core.Models;
using Microsoft.AspNetCore.Http;
public class CustomFingerprintFactory : IFingerprintFactory
{
public async Task<RequestFingerprint> CreateFingerprint(HttpContext context)
{
context.Request.EnableBuffering();
using var reader = new StreamReader(context.Request.Body, leaveOpen: true);
var body = await reader.ReadToEndAsync();
context.Request.Body.Position = 0;
var contentType = context.Request.ContentType;
var combined = $"{contentType}:{body}";
// Use your preferred hashing algorithm
var hash = ComputeHash(combined);
return new RequestFingerprint(hash);
}
private string ComputeHash(string input)
{
// Implement your hashing logic
}
}
builder.Services.AddSingleton<IFingerprintFactory, CustomFingerprintFactory>();
Custom Store Implementation
Implement your own storage backend by implementing the IIdempotencyStore interface:
using Idempotency.Core.Abstractions;
using Idempotency.Core.Models;
public class CustomIdempotencyStore : IIdempotencyStore
{
public async Task<IdempotencyClaim> ClaimAsync(
IdempotencyKey key,
RequestFingerprint fingerprint,
CancellationToken ct = default)
{
// Try to claim ownership of this idempotency key
// Return information about existing record if found
// Key components: key.ActorId, key.Scope, key.Key
// Fingerprint: fingerprint.Hash (for detecting payload conflicts)
// Example logic:
// 1. Try to insert new record with status InProgress
// 2. If insert succeeds, return IsOwner=true
// 3. If record exists, return existing record data with IsOwner=false
// IMPORTANT: This operation must be atomic to prevent race conditions
}
public async Task CompleteAsync(
IdempotencyKey key,
IdempotencyData data,
CancellationToken ct = default)
{
// Mark the request as completed and store the response data
// data.Data contains response information (status code, headers, body)
// IMPORTANT: This operation must be atomic
}
public async Task ReleaseAsync(
IdempotencyKey key,
RequestFingerprint fingerprint,
CancellationToken ct = default)
{
// Release the claim if request processing failed
// Only release if:
// - Status is InProgress
// - Fingerprint matches (same request)
// IMPORTANT: This operation must be atomic
}
}
Register your custom store:
builder.Services.AddSingleton<IIdempotencyStore, CustomIdempotencyStore>();
Key Concepts:
- IdempotencyKey: Composite key consisting of
ActorId(who),Scope(what), andKey(unique identifier) - RequestFingerprint: Hash of the request payload to detect conflicts
- IdempotencyClaim: Result of claiming a key, indicates ownership and existing data
- IdempotencyData: Response data to store (status code, headers, body)
- Atomicity: All store operations must be atomic to prevent race conditions in concurrent scenarios
License
This project is under MIT license.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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
- Idempotency.Core (>= 0.0.1)
- System.IO.Hashing (>= 10.0.1)
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 |
|---|---|---|
| 0.0.1 | 147 | 12/26/2025 |