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
                    
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="Indiko.Blocks.API.Idempotency" Version="2.1.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Indiko.Blocks.API.Idempotency" Version="2.1.2" />
                    
Directory.Packages.props
<PackageReference Include="Indiko.Blocks.API.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 Indiko.Blocks.API.Idempotency --version 2.1.2
                    
#r "nuget: Indiko.Blocks.API.Idempotency, 2.1.2"
                    
#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 Indiko.Blocks.API.Idempotency@2.1.2
                    
#: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=Indiko.Blocks.API.Idempotency&version=2.1.2
                    
Install as a Cake Addin
#tool nuget:?package=Indiko.Blocks.API.Idempotency&version=2.1.2
                    
Install as a Cake Tool

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

  1. Client sends request with unique idempotency key
  2. Middleware checks if key exists in cache
  3. If cached: Returns cached response (replay)
  4. If not cached: Processes request and caches response
  5. 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:

  1. First request: Creates order, returns 201
  2. Network timeout on client
  3. 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

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

  1. Client-Generated Keys: Let clients generate UUID keys
  2. Appropriate TTL: Match business requirements (5 min for API, 7 days for payments)
  3. HTTP Methods: Apply to POST, PUT, DELETE, PATCH
  4. Error Responses: Consider not caching 4xx/5xx errors
  5. Monitoring: Track cache hit rates
  6. 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.Abstractions
  • Indiko.Blocks.Common.Abstractions
  • Microsoft.IO.RecyclableMemoryStream

License

See LICENSE file in the repository root.

  • Indiko.Blocks.Caching.InMemory - In-memory cache backend
  • Indiko.Blocks.Caching.Redis - Redis cache backend
  • Indiko.Blocks.API.Swagger - API documentation
  • Indiko.Hosting.Web - Web API hosting
Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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