Indiko.Blocks.API.Idempotency
2.1.2
dotnet add package Indiko.Blocks.API.Idempotency --version 2.1.2
NuGet\Install-Package Indiko.Blocks.API.Idempotency -Version 2.1.2
<PackageReference Include="Indiko.Blocks.API.Idempotency" Version="2.1.2" />
<PackageVersion Include="Indiko.Blocks.API.Idempotency" Version="2.1.2" />
<PackageReference Include="Indiko.Blocks.API.Idempotency" />
paket add Indiko.Blocks.API.Idempotency --version 2.1.2
#r "nuget: Indiko.Blocks.API.Idempotency, 2.1.2"
#:package Indiko.Blocks.API.Idempotency@2.1.2
#addin nuget:?package=Indiko.Blocks.API.Idempotency&version=2.1.2
#tool nuget:?package=Indiko.Blocks.API.Idempotency&version=2.1.2
Indiko.Blocks.API.Idempotency
Idempotency middleware for ensuring API requests can be safely retried without unintended side effects.
Overview
This package provides idempotency support for HTTP APIs, allowing clients to safely retry requests without creating duplicate operations (e.g., double charges, duplicate orders).
Features
- Idempotent Requests: Prevent duplicate operations from retries
- Response Caching: Cache successful responses for replay
- Configurable TTL: Set cache duration for idempotency keys
- Memory Efficient: Uses RecyclableMemoryStreamManager
- Automatic Key Generation: Path and query-based keys
- POST/PUT/DELETE Support: Idempotency for write operations
- Distributed Cache: Works with Redis, in-memory, or any cache provider
Installation
dotnet add package Indiko.Blocks.API.Idempotency
Quick Start
Configuration (appsettings.json)
{
"IdempotencyOptions": {
"Enabled": true,
"Expiration": 24,
"CacheDuration": "Hours"
}
}
How It Works
- Client sends request with unique idempotency key
- Middleware checks if key exists in cache
- If cached: Returns cached response (replay)
- If not cached: Processes request and caches response
- Subsequent requests with same key get cached response
Basic Usage
Client-Side (Making Idempotent Requests)
// Generate unique idempotency key (UUID)
const idempotencyKey = crypto.randomUUID();
// Include in request
fetch('https://api.example.com/api/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey
},
body: JSON.stringify({
productId: '123',
quantity: 1
})
});
// If request fails, retry with SAME key
if (requestFailed) {
// Retry with same idempotency key
fetch('https://api.example.com/api/orders', {
method: 'POST',
headers: {
'Idempotency-Key': idempotencyKey // SAME KEY
},
body: JSON.stringify(...)
});
}
Server-Side Configuration
public class Startup : WebStartup
{
public override void ConfigureServices(IServiceCollection services)
{
base.ConfigureServices(services);
// Idempotency middleware is auto-configured via block system
// Requires a cache service (Redis, InMemory, etc.)
services.AddInMemoryCache(); // or AddRedisCache()
}
}
Configuration Options
IdempotencyOptions
public class IdempotencyOptions
{
// Enable/disable idempotency
public bool Enabled { get; set; } = true;
// Cache expiration value
public int Expiration { get; set; } = 24;
// Duration type
public CacheDurationType CacheDuration { get; set; } = CacheDurationType.Hours;
}
Duration Types
public enum CacheDurationType
{
Seconds,
Minutes,
Hours,
Days
}
Configuration Examples
Short Duration (API calls)
{
"IdempotencyOptions": {
"Expiration": 5,
"CacheDuration": "Minutes"
}
}
Long Duration (Financial transactions)
{
"IdempotencyOptions": {
"Expiration": 7,
"CacheDuration": "Days"
}
}
Use Cases
1. Payment Processing
[HttpPost("charge")]
public async Task<IActionResult> ChargeCustomer([FromBody] ChargeRequest request)
{
// This action is idempotent
// Multiple requests with same idempotency key = single charge
var charge = await _paymentService.ChargeAsync(request);
return Ok(charge);
}
Without Idempotency:
- Network failure after charge ? Retry ? Double charge ?
With Idempotency:
- Network failure after charge ? Retry with same key ? Cached response returned, no double charge ?
2. Order Creation
[HttpPost("orders")]
public async Task<IActionResult> CreateOrder([FromBody] CreateOrderCommand command)
{
// Idempotent order creation
var orderId = await _mediator.Send<CreateOrderCommand, Guid>(command);
return CreatedAtAction(nameof(GetOrder), new { id = orderId }, orderId);
}
Scenario:
- First request: Creates order, returns 201
- Network timeout on client
- Retry with same key: Returns cached 201 response (no duplicate order)
3. Resource Updates
[HttpPut("users/{id}")]
public async Task<IActionResult> UpdateUser(Guid id, [FromBody] UpdateUserCommand command)
{
// Idempotent update
command.UserId = id;
await _mediator.Send(command);
return NoContent();
}
Idempotency Keys
Key Generation
The middleware automatically generates keys based on:
- Request path
- Query string parameters
var key = _cacheKeyProvider.GenerateKey(
context.Request.Path,
context.Request.QueryString.Value
);
Custom Key Provider
public class CustomCacheKeyProvider : ICacheKeyProvider
{
public string GenerateKey(string path, string queryString)
{
// Custom logic (e.g., include user ID)
var userId = _httpContextAccessor.HttpContext.User.FindFirst("sub")?.Value;
return $"{userId}:{path}:{queryString}";
}
}
// Register
services.AddSingleton<ICacheKeyProvider, CustomCacheKeyProvider>();
HTTP Methods
Recommended Usage
| Method | Idempotent by Nature | Use Middleware |
|---|---|---|
| GET | Yes (read-only) | Optional |
| POST | No | Yes |
| PUT | Yes (by HTTP spec) | Yes |
| DELETE | Yes (by HTTP spec) | Yes |
| PATCH | No | Yes |
Apply to Specific Endpoints
public class IdempotencyMiddleware
{
public async Task InvokeAsync(HttpContext context)
{
// Only apply to write operations
if (context.Request.Method == "POST" ||
context.Request.Method == "PUT" ||
context.Request.Method == "DELETE" ||
context.Request.Method == "PATCH")
{
// Apply idempotency logic
}
else
{
await _next(context);
}
}
}
Cache Backends
In-Memory Cache (Development)
services.AddInMemoryCache();
Pros: Simple, no infrastructure Cons: Lost on restart, not distributed
Redis Cache (Production)
services.AddRedisCache(options =>
{
options.ConnectionString = Configuration.GetConnectionString("Redis");
});
Pros: Distributed, persistent Cons: Requires Redis server
Cached Response Structure
public class CachedResponse
{
public byte[] Content { get; set; }
public string ContentType { get; set; }
}
The middleware caches:
- Complete response body (as byte array)
- Content-Type header
Best Practices
- Client-Generated Keys: Let clients generate UUID keys
- Appropriate TTL: Match business requirements (5 min for API, 7 days for payments)
- HTTP Methods: Apply to POST, PUT, DELETE, PATCH
- Error Responses: Consider not caching 4xx/5xx errors
- Monitoring: Track cache hit rates
- Documentation: Document idempotency requirements in API docs
Security Considerations
Key Uniqueness
Ensure keys are globally unique:
{userId}-{operationId}-{timestamp}
Key Collision
Prevent key collisions by including:
- User/tenant ID
- Resource ID
- Operation type
Cache Poisoning
Validate requests before caching responses:
public async Task InvokeAsync(HttpContext context)
{
// Don't cache if validation fails
if (!IsValidRequest(context))
{
await _next(context);
return;
}
// Apply idempotency logic
}
Error Handling
Failed Requests
// Option 1: Don't cache errors
if (context.Response.StatusCode >= 400)
{
// Don't cache error responses
return;
}
// Option 2: Cache errors temporarily
if (context.Response.StatusCode >= 500)
{
// Cache server errors for short duration
await _cacheService.SetAsync(key, newResponse, TimeSpan.FromSeconds(30));
}
Cache Failures
try
{
var cachedResponse = await _cacheService.GetAsync<CachedResponse>(key);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Cache retrieval failed, processing request normally");
await _next(context);
return;
}
Performance Impact
Memory Usage
Uses RecyclableMemoryStreamManager to reduce allocations:
private static readonly RecyclableMemoryStreamManager _memoryStreamManager = new();
Benefits:
- Reduces GC pressure
- Reuses memory buffers
- Better performance for large responses
Latency
| Scenario | Latency |
|---|---|
| Cache Miss | Normal + ~1-2ms (caching overhead) |
| Cache Hit | ~1-5ms (cache retrieval) |
| Large Payload (1MB) | ~5-10ms additional |
Monitoring
Key Metrics
- Cache Hit Rate: % of requests served from cache
- Cache Miss Rate: % of requests processed normally
- Average Response Size: Monitor memory usage
- TTL Effectiveness: Track how often keys expire
Logging
public async Task InvokeAsync(HttpContext context)
{
var sw = Stopwatch.StartNew();
var key = _cacheKeyProvider.GenerateKey(...);
var cachedResponse = await _cacheService.GetAsync<CachedResponse>(key);
if (cachedResponse != null)
{
_logger.LogInformation("Idempotency cache HIT for {Key} in {Ms}ms", key, sw.ElapsedMilliseconds);
}
else
{
_logger.LogInformation("Idempotency cache MISS for {Key}", key);
}
}
Testing
Integration Test
[Fact]
public async Task Post_WithSameIdempotencyKey_ReturnsCachedResponse()
{
// Arrange
var client = _factory.CreateClient();
var idempotencyKey = Guid.NewGuid().ToString();
var request = new CreateOrderRequest { ProductId = "123" };
// Act - First request
var response1 = await client.PostAsJsonAsync("/api/orders", request);
var orderId1 = await response1.Content.ReadAsStringAsync();
// Act - Retry with same key
var response2 = await client.PostAsJsonAsync("/api/orders", request);
var orderId2 = await response2.Content.ReadAsStringAsync();
// Assert
Assert.Equal(orderId1, orderId2); // Same order ID returned
}
Target Framework
- .NET 10
Dependencies
Indiko.Blocks.Caching.AbstractionsIndiko.Blocks.Common.AbstractionsMicrosoft.IO.RecyclableMemoryStream
License
See LICENSE file in the repository root.
Related Packages
Indiko.Blocks.Caching.InMemory- In-memory cache backendIndiko.Blocks.Caching.Redis- Redis cache backendIndiko.Blocks.API.Swagger- API documentationIndiko.Hosting.Web- Web API hosting
| 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
- Indiko.Blocks.Caching.Abstractions (>= 2.1.2)
- Indiko.Blocks.Common.Abstractions (>= 2.1.2)
- Microsoft.IO.RecyclableMemoryStream (>= 3.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 |
|---|---|---|
| 2.1.2 | 268 | 12/18/2025 |
| 2.1.1 | 671 | 12/2/2025 |
| 2.1.0 | 674 | 12/2/2025 |
| 2.0.0 | 318 | 9/17/2025 |
| 1.7.23 | 192 | 9/8/2025 |
| 1.7.22 | 186 | 9/8/2025 |
| 1.7.21 | 191 | 8/14/2025 |
| 1.7.20 | 184 | 6/23/2025 |
| 1.7.19 | 185 | 6/3/2025 |
| 1.7.18 | 190 | 5/29/2025 |
| 1.7.17 | 192 | 5/26/2025 |
| 1.7.15 | 148 | 4/12/2025 |
| 1.7.14 | 164 | 4/11/2025 |
| 1.7.13 | 158 | 3/29/2025 |
| 1.7.12 | 172 | 3/28/2025 |
| 1.7.11 | 177 | 3/28/2025 |
| 1.7.10 | 177 | 3/28/2025 |
| 1.7.9 | 179 | 3/28/2025 |
| 1.7.8 | 178 | 3/28/2025 |
| 1.7.5 | 199 | 3/17/2025 |
| 1.7.4 | 189 | 3/16/2025 |
| 1.7.3 | 184 | 3/16/2025 |
| 1.7.2 | 190 | 3/16/2025 |
| 1.7.1 | 206 | 3/11/2025 |
| 1.6.8 | 219 | 3/11/2025 |
| 1.6.7 | 269 | 3/4/2025 |
| 1.6.6 | 149 | 2/26/2025 |
| 1.6.5 | 161 | 2/20/2025 |
| 1.6.4 | 162 | 2/20/2025 |
| 1.6.3 | 156 | 2/5/2025 |
| 1.6.2 | 133 | 1/24/2025 |
| 1.6.1 | 139 | 1/24/2025 |
| 1.6.0 | 122 | 1/16/2025 |
| 1.5.2 | 136 | 1/16/2025 |
| 1.5.1 | 164 | 11/3/2024 |
| 1.5.0 | 151 | 10/26/2024 |
| 1.3.2 | 155 | 10/24/2024 |
| 1.3.0 | 155 | 10/10/2024 |
| 1.2.5 | 168 | 10/9/2024 |
| 1.2.4 | 159 | 10/8/2024 |
| 1.2.1 | 157 | 10/3/2024 |
| 1.2.0 | 166 | 9/29/2024 |
| 1.1.1 | 151 | 9/23/2024 |
| 1.1.0 | 170 | 9/18/2024 |
| 1.0.33 | 170 | 9/15/2024 |
| 1.0.28 | 175 | 8/28/2024 |
| 1.0.27 | 186 | 8/24/2024 |
| 1.0.26 | 149 | 7/7/2024 |
| 1.0.25 | 166 | 7/6/2024 |
| 1.0.24 | 176 | 6/25/2024 |
| 1.0.23 | 153 | 6/1/2024 |
| 1.0.22 | 173 | 5/14/2024 |
| 1.0.21 | 155 | 5/14/2024 |
| 1.0.20 | 189 | 4/8/2024 |
| 1.0.19 | 186 | 4/3/2024 |
| 1.0.18 | 168 | 3/23/2024 |
| 1.0.17 | 194 | 3/19/2024 |
| 1.0.16 | 166 | 3/19/2024 |
| 1.0.15 | 172 | 3/11/2024 |
| 1.0.14 | 170 | 3/10/2024 |
| 1.0.13 | 181 | 3/6/2024 |
| 1.0.12 | 208 | 3/1/2024 |
| 1.0.11 | 191 | 3/1/2024 |
| 1.0.10 | 173 | 3/1/2024 |
| 1.0.9 | 168 | 3/1/2024 |