Ruvio.AspNetCore.Idempotency 0.1.0

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

Ruvio.AspNetCore.Idempotency

Opt-in HTTP result deduplication for authenticated, bounded mutation endpoints on .NET 8. Uses the native IDEM command through IRuvioClient.BeginIdempotencyAsync and CompleteIdempotencyAsync. No Lua transitions or fallback, ONCE changes, background workers, renewal timers, or automatic retries. Deploy a server supporting this native command first.

using Ruvio.AspNetCore.Idempotency;
using Ruvio.Client;

// Register your application's shared IRuvioClient separately.
builder.Services.AddSingleton<IRuvioClient>(client);
builder.Services.AddRuvioIdempotency(options =>
{
    options.Namespace = "orders-production-v1"; // stable across replicas
    options.ScopeSelector = context => context.User.FindFirst("tenant_id")?.Value;
    options.Retention = TimeSpan.FromHours(24);
    options.MaxRequestBodyBytes = 64 * 1024;
    options.MaxResponseBodyBytes = 64 * 1024;
});

app.UseRouting();
app.UseAuthentication();
app.UseAuthorization(); // MUST execute before idempotency, including on replays
app.UseRuvioIdempotency();
app.MapPost("/orders", HandleOrder)
    .RequireAuthorization()
    .WithMetadata(new RuvioIdempotencyAttribute());
// Controllers can use [RuvioIdempotency] together with [Authorize].

Isolation and HTTP contract

  • Unmarked endpoints immediately invoke the next middleware: no async state machine, body read, client resolution, or instrumentation.
  • Configure a trusted, stable tenant/user ScopeSelector and deployment Namespace. Missing configuration fails startup validation; an empty request scope throws before acquisition/handler execution. Never trust an unvalidated caller-supplied tenant header. Changes to scope/namespace create a new domain.
  • An authenticated identity with a nonempty NameIdentifier (or sub) is mandatory, otherwise 401. Its authentication type, subject issuer and subject are included in addition to your scope. Tenant-only scopes do not share results between principals. Use stable authentication claims across replicas.
  • Authentication and endpoint authorization must run before this middleware. It is not an authorization component and cannot verify your middleware order. Authorization must be reevaluated for every replay; do not put it inside the handler or after idempotency. Outer middleware may still add fresh security headers/cookies, but these are never loaded from the stored result.
  • Only POST, PUT, PATCH, DELETE are supported (other marked methods: 405). Each marked request requires one Idempotency-Key, 1–128 printable non-space ASCII characters (invalid/missing: 400). Keep keys random and unguessable.
  • SHA-256 length-delimited hashes cover namespace, authenticated subject, scope, and key for storage addressing, and method, path base, path, raw query, content type, raw body, namespace/subject/scope for the request fingerprint. Storage keys contain only a versioned prefix and one hex hash tag; no raw credentials, tenant, key, URL, or payload. Hashing is not encryption. A key reused in the same scope for different request content returns 409.
  • Responses must depend only on this fingerprinted input. Headers such as Accept and API-version headers are not fingerprinted: normalize them upstream or place a validated variant in the scope. Streaming, upgrades, trailers, changing response body features and indefinite handlers are unsupported.
  • Matching completed requests replay status, body, and only Content-Type, Content-Language, ETag, Last-Modified. Content-Length is recomputed. Cookies, authorization, cache/security headers and redirects' Location are not stored. Avoid endpoints whose result requires those headers. Normal returned error responses (including 4xx/5xx) are stored too. Thrown handler/storage errors and cancellation propagate, never become a success response, and are never automatically retried.

State and failure model (not downstream exactly-once)

Native IDEM key BEGIN ... creates a pending record with a random ownership token and one retention TTL. An existing pending request returns 409, including after crashes, cancellation, response overflow or uncertain downstream outcomes. It is not deleted, renewed, stolen or retried by this package. Completion checks the exact owner, fingerprint, pending state and positive remaining TTL, atomically stores the result and changes state to completed, preserving the original expiration. A lost owner/expired record fails completion. Nothing is sent from the captured handler body until completion is acknowledged. An identical native completion is acknowledged without rewriting; a changed result is refused. The middleware itself does not retry uncertain writes.

TTL does not guarantee downstream exactly-once. Expiration can occur while a handler still runs; a later request can execute again. Store loss, eviction, operator deletion, persistence/failover loss and namespace changes can also permit duplicates. A lost completion reply is uncertain (the server may have completed it). Downstream transactions/outboxes and downstream deduplication using a durable operation identifier remain necessary for irreversible effects. Choose retention beyond expected handler duration and retry horizon; configure appropriate Ruvio durability, ACLs and no-eviction/storage capacity. There is no distributed transaction between HTTP, Ruvio and your business database.

The server stores each transition and its original deadline in one WAL record (opcode 53); replication and recovery use the same mutation. Upgrade replicas and recovery tools as well as the server before using it. The native command keeps a packed string rather than the former Lua hash. The HTTP key prefix is unchanged to avoid silently admitting the same operation in a new namespace. Old hash records fail closed until they expire; they are not overwritten, converted or replayed via Lua. Drain old admissions, handlers and retry retention before switching all application instances. Do not delete old keys or change namespace to bypass this safety barrier. See the native contract.

Bounds and pipeline caveats

  • Defaults: 64 KiB request and response; configurable 1 byte–1 MiB each. Unknown-length request bodies are read only up to the limit plus one probe byte; oversized bodies return 413 before acquisition. No disk spooling.
  • Response stream, BodyWriter, and send-file writes are bounded before copying. Overflow is sticky even if a handler catches the exception. Request and response features/bodies are restored in finally, including failure paths. Handler status, reason phrase, and headers are isolated from the original response and copied only after completion is acknowledged; handler/store failures preserve the original response metadata for outer error handling.
  • Fingerprint fields/scope are limited to 8,192 characters each; saved response headers to 8,192 total characters. Retention is whole milliseconds from 1 ms to 7 days, matching the native command's retention bound. TimeSpan.MaxValue, negative/zero/sub-millisecond values are rejected.
  • Known-length request buffers allocate exactly the validated content length; mismatched lengths fail before acquisition. Unknown-length request buffers start at up to 4 KiB and grow only as needed, capped at the request limit. Response buffers reserve the configured response maximum after acquisition. This HTTP adapter's response envelope uses base64 (roughly 4/3 expansion), JSON and UTF-16 strings plus UTF-8 command/RESP copies. Full bounded payload duplication is unavoidable here, not zero-copy. A stored result is at most about 1.45 MiB with maximum options, below the native command's 4 MiB result limit. The native command itself is binary-safe and does not require JSON/Base64. If server protocol limits are reduced, allow for the serialized response plus the native packed header and tokens. The client may allocate a received RESP bulk before package validation; restrict access to this namespace and configure transport/server limits too.
  • Bounds are per request, not global concurrency/rate limits. Configure upstream admission/rate/request-header limits. Application allocations outside these streams are outside this package's control.
  • Handler OnStarting callbacks run before snapshot/completion (deferred until the handler finishes); flushing/starting a captured response does not send it. Middleware outside this boundary runs on every request. Put exception handling outside idempotency; if error handling is inside it, returned errors are stored. Do not let outer callbacks change the stored status/representation. Test compression/transform middleware ordering for your application.
  • Instrumentation is off by default. EnableInstrumentation enables the Ruvio.AspNetCore.Idempotency ActivitySource with an idempotency activity and only the bounded idempotency.state tag. No request/identity/key/body data.

Tests

dotnet test integrations/dotnet/Ruvio.AspNetCore.Idempotency.Tests
# Also execute native transitions against an isolated live Ruvio:
RUVIO_TEST_ADDR=127.0.0.1:6379 \
  dotnet test integrations/dotnet/Ruvio.AspNetCore.Idempotency.Tests

Live tests use unique keys and delete only their own test keys.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  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.1.0 84 10/2/2026