VOOZH about

URL: https://www.nuget.org/packages/DKNet.AspCore.Idempotency/

⇱ NuGet Gallery | DKNet.AspCore.Idempotency 10.0.27


ο»Ώ

πŸ‘ Image
DKNet.AspCore.Idempotency 10.0.27

dotnet add package DKNet.AspCore.Idempotency --version 10.0.27
 
 
NuGet\Install-Package DKNet.AspCore.Idempotency -Version 10.0.27
 
 
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="DKNet.AspCore.Idempotency" Version="10.0.27" />
 
 
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DKNet.AspCore.Idempotency" Version="10.0.27" />
 
Directory.Packages.props
<PackageReference Include="DKNet.AspCore.Idempotency" />
 
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 DKNet.AspCore.Idempotency --version 10.0.27
 
 
The NuGet Team does not provide support for this client. Please contact its maintainers for support.
#r "nuget: DKNet.AspCore.Idempotency, 10.0.27"
 
 
#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 DKNet.AspCore.Idempotency@10.0.27
 
 
#: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=DKNet.AspCore.Idempotency&version=10.0.27
 
Install as a Cake Addin
#tool nuget:?package=DKNet.AspCore.Idempotency&version=10.0.27
 
Install as a Cake Tool
The NuGet Team does not provide support for this client. Please contact its maintainers for support.

DKNet.AspCore.Idempotency

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

Overview

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:

  • Tracking Request Processing: Uses idempotency keys to identify duplicate requests
  • Caching Results: Stores successful responses in a distributed cache for re-delivery
  • Preventing Side Effects: Eliminates accidental duplicate processing from network retries or timeouts
  • Minimal Configuration: Simple setup with sensible defaults
  • Composable Design: Integrates seamlessly with ASP.NET Core's minimal API ecosystem

Key Features

✨ Core Features

  • Idempotency Key Header Support - Standard HTTP header-based idempotency key tracking
  • Distributed Caching - Uses ASP.NET Core's IDistributedCache for scalable, multi-instance support
  • Conflict Handling Strategies - Choose between returning cached results or 409 Conflict responses
  • Automatic Status Code Filtering - Only caches successful responses (2xx status codes)
  • Route-Scoped Keys - Composite keys prevent the same key being used across different endpoints
  • Configurable Expiration - TTL-based cache invalidation (default: 4 hours)
  • Security Sanitization - Input sanitization to prevent cache key injection attacks

πŸ”’ Production-Ready

  • βœ… Zero warnings (TreatWarningsAsErrors=true)
  • βœ… Nullable reference type support
  • βœ… Comprehensive XML documentation on all public APIs
  • βœ… Async/await throughout
  • βœ… Thread-safe distributed cache operations
  • βœ… Detailed structured logging

Installation

Via NuGet Package Manager

dotnet add package DKNet.AspCore.Idempotency

Via .csproj

<ItemGroup>
 <PackageReference Include="DKNet.AspCore.Idempotency" Version="*" />
</ItemGroup>

Quick Start

1. Register Idempotency Services

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();

2. Apply to Endpoints

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();

3. Send Requests with Idempotency Key

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
}

Configuration

IdempotencyOptions

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
 };
});

Conflict Handling Strategies

1. ConflictResponse (Default)

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."
}
2. CachedResult

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"
}

Behavior and Flow

Request Processing Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 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 β”‚
 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Composite Key Format

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"

Cache Key Sanitization

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

Usage Examples

Basic POST Endpoint

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);

PUT Endpoint with Custom Configuration

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();

DELETE Endpoint (No Response Body)

app.MapDelete("/orders/{id}", async (string id, IOrderService service) =>
{
 await service.DeleteOrderAsync(id);
 return Results.NoContent();
})
.RequiredIdempotentKey();

Testing

Unit Testing with TestContainers

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);
 }
}

Requirements

  • .NET: 9.0+
  • ASP.NET Core: 9.0+
  • IDistributedCache Implementation: Redis, SQL Server, MemoryCache, or other ASP.NET Core distributed cache provider

Distributed Cache Setup

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();

Logging

The filter provides detailed structured logging at various levels:

// Configure logging in appsettings.json
{
 "Logging": {
 "LogLevel": {
 "DKNet.AspCore.Idempotency": "Debug"
 }
 }
}

Log Examples

[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

API Reference

IdempotentSetup Extensions

AddIdempotency
public static IServiceCollection AddIdempotency(
 this IServiceCollection services,
 Action<IdempotencyOptions>? config = null)

Registers idempotency services into the dependency injection container.

RequiredIdempotentKey
public static RouteHandlerBuilder RequiredIdempotentKey(
 this RouteHandlerBuilder builder)

Adds the idempotency endpoint filter to a route handler.

IIdempotencyKeyRepository

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);
}

Best Practices

βœ… DO

  • Use idempotency keys on all state-modifying operations (POST, PUT, PATCH, DELETE)
  • Use UUIDs or random identifiers for idempotency keys (e.g., v4 UUIDs)
  • Configure appropriate expiration times based on your use case
  • Log idempotency events for audit trails
  • Test with duplicate requests to verify idempotent behavior
  • Use distributed cache in production for multi-instance deployments
  • Set descriptive cache key prefixes to organize cached data

❌ DON'T

  • Don't rely on sequential or predictable idempotency keys
  • Don't use extremely long expiration times (increases memory usage)
  • Don't modify the idempotency key after sending the first request
  • Don't cache error responses (only 2xx status codes are cached)
  • Don't use in-memory cache in production (won't work across instances)
  • Don't forget to configure a distributed cache provider
  • Don't expect idempotency without the header (returns 400 Bad Request)

Performance Considerations

Caching Strategy

  • Successful Responses (2xx): Cached with TTL from configuration
  • Error Responses (4xx, 5xx): Not cached, processed normally each time
  • Partial Content (206): Not cached (outside the 200-299 range in typical config)

Memory Impact

With default configuration (4-hour expiration):

  • Small response (< 1KB): ~1.5KB cached per entry
  • Medium response (1-10KB): ~10-15KB cached per entry
  • Large response (> 10KB): Recommend filtering these or using shorter TTL

Troubleshooting

Issue: "Idempotency header key is missing. Returning 400 Bad Request."

Solution: Ensure your client is sending the idempotency key header:

X-Idempotency-Key: your-unique-key

Issue: Cache entries not persisting across requests

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();

Issue: Same key works on one endpoint but not another

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"

License

Licensed under the MIT License. See LICENSE file in the project root for details.

Copyright Β© 2025 Steven Hoang. All rights reserved.

Contributing

Contributions are welcome! Please ensure:

  • Code compiles with zero warnings
  • All public APIs have XML documentation
  • Tests cover new functionality
  • Follow the DKNet framework patterns and standards

See Also


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

NuGet packages (1)

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.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
10.0.27 110 5/22/2026
10.0.26 105 5/19/2026
10.0.25 130 3/27/2026
10.0.24 118 3/27/2026
10.0.23 122 3/27/2026
10.0.22 119 3/26/2026
10.0.21 148 3/17/2026
10.0.20 132 2/2/2026