GM.Idempotency.Mediator 1.0.0

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

GM.Idempotency

Idempotency-key management for the GM.* .NET ecosystem. Make an operation safe to retry: run it at most once per key and replay the original result on duplicates — a repeated HTTP POST, a RabbitMQ redelivery, a double-clicked "Pay" button.

  • GM.Idempotency — the core IIdempotencyService, a cache-backed store, and the distributed-lock-guarded check-and-set. Depends only on GM.Caching + GM.DistributedLock.
  • GM.Idempotency.Http — an ASP.NET Core middleware that reads the Idempotency-Key header, short-circuits duplicate POST/PUT/PATCH requests, and replays the stored response.
  • GM.Idempotency.Mediator — a GM.Mediator pipeline behavior that dedups a command/message before the handler runs.

All three ship from one repo and version in lockstep.

How it works

Dedup keys live in GM.Caching (ICacheService) so lookups are fast and records expire on a TTL. The check-and-set is wrapped in a GM.DistributedLock so two near-simultaneous duplicates can't both read "not yet processed" and both run:

ExecuteAsync(key, operation):
  1. fast path   — a stored record?  → replay it, no lock taken
  2. acquire lock(key)               — a duplicate waits here for the in-flight original
  3. double-check — stored now?      → replay it
  4. run operation once, store result (+ TTL)
  5. release lock

A record stores enough of the result to replay it — not just a boolean "processed" flag — so a retried HTTP request gets the same status/body and a redelivered message gets the same response. An operation that throws is not recorded, so it stays retryable. If a duplicate can't get the lock within the wait (the original is still running), the call resolves to Pending — the HTTP layer returns 409, the Mediator layer throws so the broker redelivers.

Install

dotnet add package GM.Idempotency
dotnet add package GM.Idempotency.Http       # HTTP middleware
dotnet add package GM.Idempotency.Mediator   # GM.Mediator behavior

Core service

builder.Services.AddGMCaching();          // or AddGMRedisCaching(...)         — the dedup store
builder.Services.AddGMDistributedLock();  // or AddGMRedisDistributedLock(...) — cross-process safety
builder.Services.AddGMIdempotency(o =>
{
    o.DefaultTtl  = TimeSpan.FromHours(24); // how long a key is remembered
    o.LockExpiry  = TimeSpan.FromSeconds(30);
    o.LockWait    = TimeSpan.FromSeconds(10);
});

In-memory vs. RedisAddGMCaching() / AddGMDistributedLock() are per-process. To dedup across multiple instances (the usual production case), register the Redis backends instead. Nothing else changes.

Use the race-safe ExecuteAsync directly when you're not going through the HTTP or Mediator layers:

var execution = await idempotency.ExecuteAsync(
    key: $"charge:{command.Id}",
    operation: async ct => IdempotencyResult.Json(await ChargeAsync(command, ct)));

var result = execution.Result!.ReadJson<ChargeResult>();   // fresh on first call, replayed on a dupe
// execution.Outcome is Executed | Replayed | Pending

The three low-level primitives — IsProcessedAsync, MarkAsProcessedAsync, TryGetCachedResultAsync — are also available, but a manual check-then-mark is racy; prefer ExecuteAsync.

HTTP middleware

builder.Services.AddGMIdempotency();
builder.Services.AddGMIdempotencyHttp(o =>
{
    o.HeaderName   = "Idempotency-Key";              // default
    o.Methods      = ["POST", "PUT", "PATCH"];       // guarded verbs
    o.RequireOptIn = false;                          // true → only [Idempotent] endpoints
});

app.UseRouting();          // before the middleware, so per-endpoint [Idempotent] is visible
app.UseGMIdempotency();

A client sends the same Idempotency-Key on a retry:

POST /orders            → 201 Created            { "id": "a1b2", "serverToken": "…9f" }
POST /orders  (retry)   → 201 Created            { "id": "a1b2", "serverToken": "…9f" }
                          + Idempotency-Replayed: true      ← same body, not re-created

The stored copy keeps the status code, Content-Type, an allow-list of headers (Location by default — the created-resource URL) and the body. Only cacheable responses (2xx by default, up to a size limit) are stored, so a 500 is left to genuinely retry. Per-endpoint opt-in:

app.MapPost("/orders", CreateOrder).WithMetadata(new IdempotentAttribute { TtlSeconds = 3600 });
// or on an MVC action:  [Idempotent(TtlSeconds = 3600)]

GM.Mediator behavior (message dedup)

For at-least-once delivery (RabbitMQ via GM.Messaging), dedup by message id before the handler runs:

builder.Services.AddGMMediator(typeof(Program).Assembly);
builder.Services.AddGMIdempotency();
builder.Services.AddGMIdempotencyBehavior();   // register first → wraps your other behaviors

// Opt a command in with its message id (the "derived from the message" strategy):
public sealed record ProcessPayment(string MessageId, decimal Amount)
    : IRequest<PaymentResult>, IIdempotentRequest
{
    public string IdempotencyKey => MessageId;
}

A redelivery of MessageId is recognized: the handler runs once, and the redelivery gets the stored PaymentResult back. Requests that don't opt in pass through untouched.

Key strategies

The key is deliberately flexible — pick per integration:

Strategy Where How
Caller-supplied HTTP the Idempotency-Key header (client owns it, resends on retry)
Derived from the message Mediator IIdempotentRequest.IdempotencyKey → the message/command id
Hash of the payload Mediator [Idempotent] on a request with no natural id → IdempotencyKey.FromPayload(...)

IIdempotencyKeyProvider<TSource> is the extension point if you need a different source; the IdempotencyKey helpers (FromHash, FromPayload, Compose) are the building blocks.

Follow-up integration points (flagged, not built)

These are candidates, called out for a deliberate decision rather than wired up here:

  • GM.HttpClient outbound retry — should its retry policy generate and attach an Idempotency-Key to outbound POST/PUTs (and reuse it across retries) so downstream services can dedup our calls? A natural pairing, but it changes outbound contracts — decide per upstream.
  • GM.Payments — the highest-value consumer (never double-charge). Likely wants the Mediator behavior on payment commands and to propagate a provider-level idempotency key to the payment gateway. Needs a payments-domain decision on key derivation and TTL.
  • GM.KYC webhook handlers — inbound webhooks are retried by providers; dedup by the provider's event id via the Mediator behavior (or a thin HTTP guard). Confirm each provider's event-id header.

Options

IdempotencyOptions (core): KeyPrefix, DefaultTtl, LockExpiry, LockWait, LockRetryInterval. IdempotencyHttpOptions: HeaderName, Methods, RequireOptIn, ScopeKeyToEndpoint, IsCacheableStatusCode, ReplayResponseHeaders, MaxReplayableBodyBytes, ReplayedHeaderName, ConflictStatusCode.

Samples

Runnable API demonstrating both integration points: GM.Idempotency.Samples.

License

MIT — see 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
1.0.0 79 8/3/2026