JacobVW.Retry
10.7.4
dotnet add package JacobVW.Retry --version 10.7.4
NuGet\Install-Package JacobVW.Retry -Version 10.7.4
<PackageReference Include="JacobVW.Retry" Version="10.7.4" />
<PackageVersion Include="JacobVW.Retry" Version="10.7.4" />
<PackageReference Include="JacobVW.Retry" />
paket add JacobVW.Retry --version 10.7.4
#r "nuget: JacobVW.Retry, 10.7.4"
#:package JacobVW.Retry@10.7.4
#addin nuget:?package=JacobVW.Retry&version=10.7.4
#tool nuget:?package=JacobVW.Retry&version=10.7.4
JacobVW.Retry
DB-backed retry service with timestamp-aware idempotency for .NET applications.
Features
- Pluggable storage backend — ships with EF Core, implement
IRetryStorefor Redis/MongoDB/etc. - Timestamp-aware deduplication — out-of-order events are stored as
Discardedfor auditing - Same-timestamp support — multiple events at the same timestamp are all processed;
CreatedAtis used as a tiebreaker - Expiry mechanism — operations stop retrying after a configurable deadline
- Supersede detection — older events are marked
Supersededwhen newer ones complete - Exponential backoff with jitter — prevents thundering herd on retries
- Pluggable handlers — implement
IRetryOperationHandlerfor 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→RetryServiceRetryProcessorService(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:
Discardedevents are caught at enqueue time (never processed).Supersededevents 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):
- Enqueue — all same-timestamp events are accepted. Only a strictly newer timestamp blocks enqueue.
- Processing —
CreatedAt(set automatically when the operation is created) acts as a tiebreaker. If a same-timestamp event with a laterCreatedAthas already completed, earlier-created events are markedSuperseded. - 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 | Versions 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. |
-
net10.0
- Microsoft.EntityFrameworkCore.Relational (>= 10.0.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.1)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.1)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.1)
-
net9.0
- Microsoft.EntityFrameworkCore.Relational (>= 9.0.3)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.3)
- Microsoft.Extensions.Hosting.Abstractions (>= 9.0.3)
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.3)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.