Idempotency.AspNetCore 0.0.1

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

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 TimeToLive setting
  • Collection is created automatically if it doesn't exist

How It Works

  1. Client sends request with X-Idempotency-Key header
  2. Middleware intercepts requests to endpoints marked with .WithIdempotency()
  3. Actor identification resolves who is making the request (user ID, anonymous, etc.)
  4. Claim attempt tries to claim ownership of the idempotency key
  5. Fingerprint check compares request payload hash with stored fingerprint
  6. 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
  7. Response stored (if successful and matches storage criteria)
  8. 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), and Key (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 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
0.0.1 147 12/26/2025