JacobVW.Retry 10.7.4

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

JacobVW.Retry

DB-backed retry service with timestamp-aware idempotency for .NET applications.

Features

  • Pluggable storage backend — ships with EF Core, implement IRetryStore for Redis/MongoDB/etc.
  • Timestamp-aware deduplication — out-of-order events are stored as Discarded for auditing
  • Same-timestamp support — multiple events at the same timestamp are all processed; CreatedAt is used as a tiebreaker
  • Expiry mechanism — operations stop retrying after a configurable deadline
  • Supersede detection — older events are marked Superseded when newer ones complete
  • Exponential backoff with jitter — prevents thundering herd on retries
  • Pluggable handlers — implement IRetryOperationHandler for each operation type
  • Query API — check failed, expired, superseded, and discarded operations; get status counts
  • Re-enqueue expired — retry expired operations with a new deadline

Installation

dotnet add package JacobVW.Retry

Or as a project reference:

<ProjectReference Include="../JacobVW.Retry/JacobVW.Retry.csproj" />

Setup

1. Implement IRetryDbContext on your DbContext

public class MyDbContext : DbContext, IRetryDbContext
{
    public DbSet<RetryableOperation> RetryableOperations { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfiguration(new RetryableOperationConfiguration());
    }
}

2. Register services

builder.Services.AddRetryService<MyDbContext>();

This registers:

  • IRetryStore → EfCoreRetryStore (default storage backend)
  • IRetryService → RetryService
  • RetryProcessorService (background worker)

3. Create a handler

public class StockUpdateHandler : IRetryOperationHandler
{
    public static string OperationName => "StockUpdate";

    public async Task HandleAsync(RetryableOperation operation, CancellationToken ct)
    {
        var payload = JsonSerializer.Deserialize<StockPayload>(operation.SerializedPayload!);
        // Do work — throw on failure to trigger retry
    }
}

4. Register your handler

builder.Services.AddRetryHandler<StockUpdateHandler>();

5. Enqueue operations

await retryService.EnqueueAsync(
    operationName: "StockUpdate",
    entityKey: "BMW:51357339591",
    eventTimestamp: DateTimeOffset.UtcNow,
    serializedPayload: JsonSerializer.Serialize(payload),
    maxRetries: 5,
    maxFailedDuration: TimeSpan.FromHours(12));

Query API

// Get permanently failed operations (exhausted all retries)
var failed = await retryService.GetFailedAsync(operationName: "StockUpdate");

// Get expired operations (past deadline)
var expired = await retryService.GetExpiredAsync();

// Get superseded operations (skipped — newer event already completed)
var superseded = await retryService.GetSupersededAsync();

// Get discarded operations (stale events stored for auditing)
var discarded = await retryService.GetDiscardedAsync();

// Get counts by status
var counts = await retryService.GetStatusCountsAsync();
// counts[RetryStatus.Pending], counts[RetryStatus.Failed], etc.

Re-enqueue Expired Operations

// Retry all expired operations with a new 24h deadline (default)
int count = await retryService.RetryExpiredAsync();

// Retry a single expired operation by ID
await retryService.RetryExpiredAsync(operationId: someGuid);

// Retry all expired of a specific type with a custom deadline
await retryService.RetryExpiredAsync(
    operationName: "StockUpdate",
    newMaxFailedDuration: TimeSpan.FromHours(4));

Operation Statuses

Status Meaning
Pending Waiting to be processed
InProgress Currently being handled
Completed Successfully processed
Failed Handler threw — will retry if attempts remain
Expired Past ExpiresAt deadline, stopped retrying
Superseded A newer event for the same entity completed during processing
Discarded Stale event — a strictly newer event already existed at enqueue time

Superseded vs Discarded: Discarded events are caught at enqueue time (never processed). Superseded events were queued first but a newer event completed before they were processed.

Same-Timestamp Events

When multiple events arrive for the same entity with identical EventTimestamp values (e.g. a webhook provider fires multiple updates simultaneously):

  1. Enqueue — all same-timestamp events are accepted. Only a strictly newer timestamp blocks enqueue.
  2. Processing — CreatedAt (set automatically when the operation is created) acts as a tiebreaker. If a same-timestamp event with a later CreatedAt has already completed, earlier-created events are marked Superseded.
  3. Handlers should be idempotent — since same-timestamp events both run, handlers should fetch the latest state from the source API rather than relying on the webhook payload.
Event A (timestamp=T, CreatedAt=09:00:01) ─┐
                                            ├─ Both enqueued as Pending
Event B (timestamp=T, CreatedAt=09:00:02) ─┘

If B completes first → A is Superseded (B has later CreatedAt)
If A completes first → B also completes  (B has later CreatedAt, not superseded)

Custom Storage Backend

To use a different backend (Redis, MongoDB, etc.), implement IRetryStore and use the non-generic AddRetryService() overload:

builder.Services.AddRetryService(); // no EF Core store registered
builder.Services.AddScoped<IRetryStore, RedisRetryStore>();

IRetryStore has 11 methods — 8 queries and 3 mutations. Each mutation must persist immediately (no SaveChanges concept).

Configuration

// Custom processing interval (default: 30 seconds)
builder.Services.AddRetryService(processingInterval: TimeSpan.FromSeconds(10));

Supported Frameworks

  • .NET 9.0
  • .NET 10.0

License

MIT

Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  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 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
10.7.4 861 3/11/2026
10.7.3 134 3/6/2026
10.7.2 114 3/4/2026
10.7.1 134 2/27/2026
10.7.0 127 2/27/2026
10.6.0 142 2/26/2026
10.5.0 119 2/26/2026
10.4.0 133 2/17/2026
10.3.0 117 2/17/2026
10.2.0 116 2/17/2026
10.1.0 118 2/17/2026
10.0.0 120 2/17/2026