Ruvio.Extensions.Idempotency 0.1.0

There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package Ruvio.Extensions.Idempotency --version 0.1.0
                    
NuGet\Install-Package Ruvio.Extensions.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.Extensions.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.Extensions.Idempotency" Version="0.1.0" />
                    
Directory.Packages.props
<PackageReference Include="Ruvio.Extensions.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.Extensions.Idempotency --version 0.1.0
                    
#r "nuget: Ruvio.Extensions.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.Extensions.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.Extensions.Idempotency&version=0.1.0
                    
Install as a Cake Addin
#tool nuget:?package=Ruvio.Extensions.Idempotency&version=0.1.0
                    
Install as a Cake Tool

<p align="center"> <a href="https://ruviodb.com/"><img src="https://raw.githubusercontent.com/salihcantekin/ruvio/main/docs/images/ruvio-mark.png" width="88" alt="Ruvio"></a> </p>

<h1 align="center">Ruvio.Extensions.Idempotency <sub>NuGet</sub></h1>

<p align="center">Run work once per key in message consumers, background jobs and gRPC handlers; a repeat gets the stored result.</p>

<p align="center"> <a href="https://www.nuget.org/packages/Ruvio.Extensions.Idempotency"><img alt="NuGet" src="https://img.shields.io/nuget/v/Ruvio.Extensions.Idempotency?label=nuget"></a> <a href="https://www.nuget.org/packages/Ruvio.Extensions.Idempotency"><img alt="Downloads" src="https://img.shields.io/nuget/dt/Ruvio.Extensions.Idempotency?label=downloads"></a> <a href="https://github.com/salihcantekin/ruvio/blob/main/clients/LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-green"></a> </p>

<p align="center"> <a href="https://github.com/salihcantekin/ruvio/blob/main/integrations/dotnet/README.md">All .NET integrations</a> · <a href="https://github.com/salihcantekin/ruvio/blob/main/clients/csharp/Ruvio.Client/README.md">Ruvio.Client</a> · <a href="https://github.com/salihcantekin/ruvio">Ruvio server</a> </p>


Queues deliver a message more than once, jobs get retried, and clients resend calls. This package runs the work for a key once and gives every repeat the stored result, using Ruvio's native IDEM command. It has no ASP.NET Core dependency; for HTTP endpoints use Ruvio.AspNetCore.Idempotency.

builder.Services.AddRuvioClient(builder.Configuration.GetSection("Ruvio"));
builder.Services.AddRuvioIdempotentExecutor(o => o.KeyPrefix = "payments:");

// In a consumer:
IdempotentResult<Receipt> result = await executor.ExecuteOnceAsync(
    key: message.Id,
    fingerprint: message.ContentHash,                  // null when the id alone identifies the work
    work: ct => ChargeAsync(message, ct),
    typeInfo: AppJson.Default.Receipt,                 // source-generated JSON, trimming and AOT safe
    retention: TimeSpan.FromDays(1),
    cancellationToken);

switch (result.Outcome)
{
    case IdempotentOutcome.Executed:                   // ran now
    case IdempotentOutcome.Replayed:                   // ran before; result.Value is the stored receipt
        await message.CompleteAsync();
        break;
    case IdempotentOutcome.Pending:                    // another consumer is running it, or a run failed
        await message.AbandonAsync();                  // try again later
        break;
    case IdempotentOutcome.Conflict:                   // same id, different content
        await message.DeadLetterAsync();
        break;
}
Overload Stores
ExecuteOnceAsync(key, fingerprint, ct => Task<byte[]>) the bytes as they are
ExecuteOnceAsync<T>(key, fingerprint, ct => Task<T>, JsonTypeInfo<T>) T as UTF-8 JSON
ExecuteOnceAsync(key, fingerprint, ct => Task) nothing (an empty result); returns the IdempotentOutcome

How it works

One call is one IDEM key BEGIN fingerprint owner retention and, only when it owns the record, the work and one IDEM key COMPLETE fingerprint owner result. The owner is a new random token per call, so only that call can complete the record. A repeat while the work runs gets Pending; after it, Replayed with the result; with another fingerprint, Conflict. The record and its result live for the retention (1 ms to 7 days, whole milliseconds; DefaultRetention is 1 day) and are then forgotten: a repeat after that runs the work again.

Results are limited to 4 MiB by the server. Keys are KeyPrefix + your key; reserve that prefix for this package, because ordinary writes (SET, DEL, expiry changes) to those keys break the guarantees. ClientKey selects a connection registered with AddKeyedRuvioClient.

Failures: at most once, not exactly once

  • The work throws. The exception reaches you and, by default, the record stays pending until its retention ends. Calls in that time get Pending and do not run the work. Choose the retention with this in mind: a short one lets a failed message be processed again sooner, a long one catches repeats for longer. To free the key at once instead, see below.
  • The work outlives the retention. The record expired while it ran, so the result is not stored: Executed comes back with Stored = false, and another call may already have run the work too.
  • The process dies mid-work, or Ruvio is unreachable. Like a throw: pending until the retention ends. A storage error after the work ran is raised and never retried, because the result may have been stored.

Freeing the key when the work throws

builder.Services.AddRuvioIdempotentExecutor(o =>
{
    o.KeyPrefix = "payments:";
    o.AbortOnException = true;
});

With AbortOnException (off by default; it needs a server that has IDEM ABORT, otherwise one warning is logged and nothing is freed), a work function that throws or is cancelled makes the executor send IDEM key ABORT fingerprint owner before the exception reaches you, so the next call with that key runs the work at once. The server removes the record only while it is still pending with this call's owner, so a result stored by another call is never removed. Turn it on only for work that throws before it takes effect, or that is safe to run again: the next call runs it again. If the abort itself fails (Ruvio unreachable) the work's exception still reaches you and the record stays pending until its retention ends. Needs a server with IDEM ... ABORT. Nothing is sent when the work succeeds or when the option is off.

Side effects outside Ruvio (a payment, an email) still happen once per run of the work. Pass the key to the external system as its own idempotency key where it has one.

Telemetry

Meter Ruvio.Extensions.Idempotency, counter ruvio.idempotency.outcomes tagged outcome (executed, replayed, pending, conflict, and aborted for a record freed by AbortOnException). No keys or results are recorded; with no listener a call pays one field read.

Startup checks

A configuration mistake stops the host while it starts, before the server takes a request, with the fix named in the error. This package checks:

  • the client is registered: AddRuvioClient(...), or AddKeyedRuvioClient(key, ...) when ClientKey is set. Only the registration is checked and no connection is opened, so a Ruvio that is down at start does not stop it. Skipped when an executor was registered first, which then stays.

See Startup checks for how the checks run.

Tests

dotnet test integrations/dotnet/Ruvio.Extensions.Idempotency.Tests
# Also run against a live Ruvio:
RUVIO_TEST_ADDR=127.0.0.1:6379 dotnet test integrations/dotnet/Ruvio.Extensions.Idempotency.Tests
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.1-preview.6 46 10/28/2026
0.1.0 32 10/29/2026