![]() |
VOOZH | about |
dotnet add package DKNet.AspCore.Idempotency --version 10.0.27
NuGet\Install-Package DKNet.AspCore.Idempotency -Version 10.0.27
<PackageReference Include="DKNet.AspCore.Idempotency" Version="10.0.27" />
<PackageVersion Include="DKNet.AspCore.Idempotency" Version="10.0.27" />Directory.Packages.props
<PackageReference Include="DKNet.AspCore.Idempotency" />Project file
paket add DKNet.AspCore.Idempotency --version 10.0.27
#r "nuget: DKNet.AspCore.Idempotency, 10.0.27"
#:package DKNet.AspCore.Idempotency@10.0.27
#addin nuget:?package=DKNet.AspCore.Idempotency&version=10.0.27Install as a Cake Addin
#tool nuget:?package=DKNet.AspCore.Idempotency&version=10.0.27Install as a Cake Tool
A robust, production-ready idempotency middleware for ASP.NET Core minimal APIs and endpoints. This library prevents duplicate request processing by enforcing idempotent request semantics using distributed caching.
π .NET
π License
π NuGet
Idempotency is a critical feature for API design, especially for operations that modify state (POST, PUT, PATCH, DELETE). This library provides an elegant way to implement idempotent endpoints in ASP.NET Core by:
IDistributedCache for scalable, multi-instance supportTreatWarningsAsErrors=true)dotnet add package DKNet.AspCore.Idempotency
<ItemGroup>
<PackageReference Include="DKNet.AspCore.Idempotency" Version="*" />
</ItemGroup>
In your Program.cs, register the idempotency services with dependency injection:
var builder = WebApplicationBuilder.CreateBuilder(args);
// Add idempotency services
builder.Services.AddIdempotency(options =>
{
options.IdempotencyHeaderKey = "X-Idempotency-Key"; // default
options.CachePrefix = "idem"; // default
options.Expiration = TimeSpan.FromHours(4); // default
options.ConflictHandling = IdempotentConflictHandling.ConflictResponse; // default
options.JsonSerializerOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
});
// Add distributed cache (required)
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
});
var app = builder.Build();
Use the RequiredIdempotentKey() filter on endpoints that should be idempotent:
// POST endpoint with idempotency
app.MapPost("/orders", CreateOrderAsync)
.WithName("CreateOrder")
.WithOpenApi()
.RequiredIdempotentKey(); // <- Add idempotency filter
// PUT endpoint with idempotency
app.MapPut("/orders/{id}", UpdateOrderAsync)
.WithName("UpdateOrder")
.RequiredIdempotentKey();
app.Run();
Clients send requests with the idempotency key header:
POST /orders HTTP/1.1
Host: api.example.com
X-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{
"productId": 123,
"quantity": 5,
"customerId": 456
}
Customize idempotency behavior through IdempotencyOptions:
builder.Services.AddIdempotency(options =>
{
// HTTP header name for idempotency keys
// Default: "X-Idempotency-Key"
options.IdempotencyHeaderKey = "Idempotency-Key";
// Prefix for all cache keys to prevent collisions
// Default: "idem"
options.CachePrefix = "myapp-idempotency";
// Cache entry expiration time
// Default: 4 hours
// Requests with expired keys are treated as new requests
options.Expiration = TimeSpan.FromHours(24);
// How to handle duplicate requests
// Default: ConflictResponse (returns 409 Conflict)
options.ConflictHandling = IdempotentConflictHandling.CachedResult; // Return cached response instead
// JSON serialization options for response caching
// Used when ConflictHandling is set to CachedResult
options.JsonSerializerOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
};
});
Returns an HTTP 409 Conflict response when a duplicate request is detected:
HTTP/1.1 409 Conflict
Content-Type: application/problem+json
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.8",
"title": "Conflict",
"status": 409,
"detail": "The request with the same idempotent key `550e8400-e29b-41d4-a716-446655440000` has already been processed."
}
Returns the cached response from the original request:
HTTP/1.1 200 OK
Content-Type: application/json
{
"orderId": 789,
"status": "created",
"createdAt": "2025-01-30T10:30:00Z"
}
βββββββββββββββββββββββββββββββββββββββ
β Request arrives with or without β
β Idempotency-Key header β
ββββββββββββββ¬βββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββ
β Is Idempotency-Key header present? β
ββββββββββ¬ββββββββββββββββββββ¬βββββββββ
β No β Yes
βΌ βΌ
βββββββββββββββ ββββββββββββββββββββββ
β Return 400 β β Check cache for β
β Bad Request β β composite key β
βββββββββββββββ ββββββββββ¬ββββββββββββ
β
ββββββββββββ΄βββββββββββ
β Found β Not Found
βΌ βΌ
βββββββββββββββββββ ββββββββββββββββββββ
β Check conflict β β Process request β
β handling β β normally β
ββββββ¬βββββββββββββ ββββββ¬ββββββββββββββ
β β
βββββββββ΄βββββββββ β
β β β
Conflict Cached βΌ
Response Result ββββββββββββββββββββ
β β β Is status code β
β β β 2xx (success)? β
β β ββββββ¬βββββββββββ¬βββ
β β β β
β β Yes No
β β β β
β β βΌ βΌ
β β ββββββββββ ββββββββ
β β β Cache β β Don'tβ
β β β result β βcache β
β β ββββββββββ ββββββββ
β β β β
ββββββββββββββββββ΄ββββββββ΄βββββββββββ
β
βΌ
ββββββββββββββββββββ
β Return response β
β to client β
ββββββββββββββββββββ
The filter creates a composite key from the route template and idempotency key to support the same idempotency key being used across different endpoints:
CompositeKey = "{routeTemplate}_{idempotencyKey}"
Examples:
- Route: POST /orders, Key: abc-123 β "POST /orders_abc-123"
- Route: PUT /users/{id}, Key: abc-123 β "PUT /users/{id}_abc-123"
User-provided idempotency keys are sanitized to prevent cache key injection:
// Input: "abc-123/../../malicious"
// Sanitized: "idem_abc-123__________malicious" (uppercase)
// Characters removed/replaced:
// "/" β "_"
// "\n" β removed
// "\r" β removed
// Result is uppercased for consistency
app.MapPost("/orders", async (CreateOrderRequest request, IOrderService service) =>
{
var order = await service.CreateOrderAsync(request);
return Results.Created($"/orders/{order.Id}", order);
})
.Produces<OrderResponse>(StatusCodes.Status201Created)
.RequiredIdempotentKey();
public record CreateOrderRequest(string ProductId, int Quantity);
public record OrderResponse(string OrderId, string Status, DateTime CreatedAt);
builder.Services.AddIdempotency(options =>
{
options.IdempotencyHeaderKey = "Request-Id";
options.ConflictHandling = IdempotentConflictHandling.CachedResult;
options.Expiration = TimeSpan.FromHours(24);
});
app.MapPut("/users/{id}", async (string id, UpdateUserRequest request, IUserService service) =>
{
var user = await service.UpdateUserAsync(id, request);
return Results.Ok(user);
})
.Produces<UserResponse>()
.RequiredIdempotentKey();
app.MapDelete("/orders/{id}", async (string id, IOrderService service) =>
{
await service.DeleteOrderAsync(id);
return Results.NoContent();
})
.RequiredIdempotentKey();
The library includes integration tests using TestContainers for SQL Server and Redis:
[Collection("Redis Collection")]
public class IdempotencyEndpointTests : IAsyncLifetime
{
private readonly ApiFixture _fixture;
public IdempotencyEndpointTests()
{
_fixture = new ApiFixture();
}
public async Task InitializeAsync() => await _fixture.InitializeAsync();
public async Task DisposeAsync() => await _fixture.DisposeAsync();
[Fact]
public async Task CreateOrder_WithValidIdempotencyKey_Returns201Created()
{
// Arrange
var idempotencyKey = Guid.NewGuid().ToString();
var request = new CreateOrderRequest("PROD-001", 5);
// Act
var response = await _fixture.HttpClient!.PostAsJsonAsync(
"/orders",
request,
headers => headers.Add("X-Idempotency-Key", idempotencyKey)
);
// Assert
response.StatusCode.Should().Be(StatusCodes.Status201Created);
}
[Fact]
public async Task CreateOrder_WithDuplicateIdempotencyKey_Returns409Conflict()
{
// Arrange
var idempotencyKey = Guid.NewGuid().ToString();
var request = new CreateOrderRequest("PROD-001", 5);
// Act - First request
var firstResponse = await _fixture.HttpClient!.PostAsJsonAsync(
"/orders",
request,
headers => headers.Add("X-Idempotency-Key", idempotencyKey)
);
// Act - Duplicate request
var secondResponse = await _fixture.HttpClient!.PostAsJsonAsync(
"/orders",
request,
headers => headers.Add("X-Idempotency-Key", idempotencyKey)
);
// Assert
firstResponse.StatusCode.Should().Be(StatusCodes.Status201Created);
secondResponse.StatusCode.Should().Be(StatusCodes.Status409Conflict);
}
}
You must register an IDistributedCache implementation. Choose one:
// Redis (Recommended for production)
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost:6379";
});
// SQL Server
builder.Services.AddDistributedSqlServerCache(options =>
{
options.ConnectionString = builder.Configuration.GetConnectionString("DefaultConnection");
options.SchemaName = "dbo";
options.TableName = "DistributedCache";
});
// In-Memory (Development only)
builder.Services.AddDistributedMemoryCache();
The filter provides detailed structured logging at various levels:
// Configure logging in appsettings.json
{
"Logging": {
"LogLevel": {
"DKNet.AspCore.Idempotency": "Debug"
}
}
}
[Debug] Checking idempotency header key: X-Idempotency-Key
[Debug] Trying to get existing result for cache key: IDEM_550E8400-E29B-41D4-A716-446655440000
[Debug] Existing result found: null
[Debug] Returning result to the client
[Info] Caching the response for idempotency key: 550e8400-e29b-41d4-a716-446655440000
public static IServiceCollection AddIdempotency(
this IServiceCollection services,
Action<IdempotencyOptions>? config = null)
Registers idempotency services into the dependency injection container.
public static RouteHandlerBuilder RequiredIdempotentKey(
this RouteHandlerBuilder builder)
Adds the idempotency endpoint filter to a route handler.
Core repository interface for managing idempotency keys:
public interface IIdempotencyKeyRepository
{
/// Checks if the key has been processed
ValueTask<(bool processed, string? result)> IsKeyProcessedAsync(string idempotencyKey);
/// Marks the key as processed with optional result
ValueTask MarkKeyAsProcessedAsync(string idempotencyKey, string? result = null);
}
With default configuration (4-hour expiration):
Solution: Ensure your client is sending the idempotency key header:
X-Idempotency-Key: your-unique-key
Solution: Verify a distributed cache is properly configured:
// Check appsettings.json has valid Redis/SQL connection
// Or register in-memory cache if testing locally
builder.Services.AddDistributedMemoryCache();
Solution: This is expected behavior. Composite keys include the route, so the same key can be used across different endpoints:
POST /orders + key ABC = "POST /orders_ABC" (different from)
PUT /orders/{id} + key ABC = "PUT /orders/{id}_ABC"
Licensed under the MIT License. See LICENSE file in the project root for details.
Copyright Β© 2025 Steven Hoang. All rights reserved.
Contributions are welcome! Please ensure:
Version: 10.0+ | Status: Production Ready | Last Updated: January 30, 2026
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 net10.0 is compatible. net10.0-android net10.0-android was computed. net10.0-browser net10.0-browser was computed. net10.0-ios net10.0-ios was computed. net10.0-maccatalyst net10.0-maccatalyst was computed. net10.0-macos net10.0-macos was computed. net10.0-tvos net10.0-tvos was computed. net10.0-windows net10.0-windows was computed. |
Showing the top 1 NuGet packages that depend on DKNet.AspCore.Idempotency:
| Package | Downloads |
|---|---|
|
DKNet.AspCore.Idempotency.MsSqlStore
DKNet is an enterprise-grade .NET library collection focused on advanced EF Core extensions, dynamic predicate building, and the Specification pattern. It provides production-ready tools for building robust, type-safe, and testable data access layers, including dynamic LINQ support, LinqKit integration. Designed for modern cloud-native applications, DKNet enforces strict code quality, async best practices, and full documentation for all public APIs. Enterprise-grade .NET library suite for modern application development, featuring advanced EF Core extensions (dynamic predicates, specifications, LinqKit), robust Domain-Driven Design (DDD) patterns, and domain event support. DKNet empowers scalable, maintainable, and testable solutions with type-safe validation, async/await, XML documentation, and high code quality standards. Ideal for cloud-native, microservices, and enterprise architectures. |
This package is not used by any popular GitHub repositories.