Resilience · SQL fault detection · data access

Your save survived
the deadlock.

Cloud databases drop connections, pick deadlock victims and throttle. Transformations retries the right failures with exponential backoff, fails fast on the rest, and wraps Dapper and EF Core so you stop hand-writing the same retry loop.

✓ Sync and async ✓ Backoff with jitter ✓ .NET 8, 9, and 10
retry://orders.save illustrative
CALL context.SaveChangesWithRetryAsync()
attempt 1 SqlException 1205 · deadlock victim transient
wait 200 ms ×2
attempt 2 SqlException -2 · client timeout transient
wait 400 ms ×2
attempt 3 3 entries written success
RETRIES3 default
FIRST DELAY200 ms
BACKOFFdoubles
30SQL Server and Azure SQL error numbers classified as transient
2wrappers: Dapper queries and EF Core SaveChanges
0.0–1.0jitter factor to stop synchronized retry storms
sync + asyncthe same options on both paths, with cancellation
Retry with backoff

The retry loop you keep rewriting, written once.

Resilience.Retry and RetryAsync run an operation, wait, double the delay and try again, up to the retry count you choose. Filter by exception type, fail fast on the ones that will never succeed, add jitter, and get a callback before every wait.

RetryWait rangeRelative

The API

Small surface. Every knob you actually need.

The same options exist for synchronous and asynchronous work. Pick the shape that matches how much control you want over which failures are worth retrying.

Plain retry with jitter

Three retries starting at 200 ms, with a quarter of random spread so many clients do not retry in lockstep.

var order = Resilience.Retry(
    () => repo.Load(id),
    retryCount: 3,
    initialDelay: TimeSpan.FromMilliseconds(200),
    jitterFactor: 0.25);
Best for: simple, idempotent reads

Retry only what can recover

Name the exception types worth another attempt, and the ones that must surface immediately.

Resilience.Retry(
    () => client.Send(request),
    retryCount: 5,
    initialDelay: TimeSpan.FromMilliseconds(100),
    retryOnExceptions: new[] { typeof(HttpRequestException) },
    failFastExceptions: new[] { typeof(ArgumentException) });
Best for: HTTP and service calls

Async with a predicate

Decide per exception. Anything the predicate rejects is rethrown immediately, original exception intact.

var rows = await Resilience.RetryAsync(
    () => db.LoadAsync(ct),
    retryCount: 4,
    initialDelay: TimeSpan.FromMilliseconds(250),
    shouldRetry: ex => ex is TimeoutException,
    cancellationToken: ct);
Best for: custom classification

Know when it retries

A callback runs before each wait with the attempt number, the exception, the delay and the elapsed time.

await Resilience.RetryAsync(
    () => queue.PublishAsync(msg),
    retryCount: 3,
    initialDelay: TimeSpan.FromMilliseconds(200),
    onRetryAsync: ctx => log.WarnAsync(
        $"attempt {ctx.AttemptNumber} failed, waiting {ctx.DelayBeforeNextAttempt}"),
    cancellationToken: ct);
Best for: logging and metrics

Retry re-runs the whole operation. Only wrap work that is safe to repeat: reads, idempotent writes, or a single atomic statement. Cancellation is never retried, and the delay between attempts is cancellable.

SQL fault detection

Thirty error numbers that deserve a second try.

SqlTransientFaultDetector.IsTransient follows Microsoft's retry guidance for SQL Server and Azure SQL. A deadlock victim or a throttled elastic pool is worth retrying; a unique-key violation or a syntax error never will be. A TimeoutException also counts as transient.

Is this error retried?

Type a SQL error number, or pick one from the groups. Anything outside the set fails fast.

Try 1205, 40613, or 2627.
Dapper and EF Core

Swap one method. Keep your code.

The wrappers apply the retry engine to the calls you already make. The defaults are three retries starting at 200 ms, and the delay doubles each time.

Dapper queries

Replace QueryAsync with QueryWithRetryAsync. Only faults the detector classifies as transient are retried; everything else surfaces at once.

var users = await connection.QueryWithRetryAsync<User>(
    "SELECT * FROM Users WHERE Active = @Active",
    new { Active = true });

int affected = await connection.ExecuteWithRetryAsync(
    "DELETE FROM Sessions WHERE ExpiredAt < @Now",
    new { Now = DateTime.UtcNow });
Also: single row, scalar

EF Core SaveChanges

Retry with backoff, and fail fast on DbUpdateConcurrencyException, because repeating a conflicting write fails the same way.

int saved = await context.SaveChangesWithRetryAsync();

await context.SaveChangesWithRetryAsync(
    retryCount: 5,
    initialDelay: TimeSpan.FromMilliseconds(100),
    retryOnExceptions: new[] { typeof(TimeoutException) },
    failFastExceptions: null);

The default overload retries any exception except the concurrency conflict. Pass retryOnExceptions to narrow it to the faults you trust.

Audit what changed

Capture added, modified and deleted properties, with key values and a UTC timestamp, before the save resets the change tracker.

var audit = context.GetAuditEntries(EntityState.Modified);
await context.SaveChangesWithRetryAsync();

foreach (var e in audit)
    log.Info("{0}.{1}: {2} -> {3}",
        e.EntityType, e.PropertyName, e.OriginalValue, e.CurrentValue);
Best for: audit trails

Look before you leap

Use the detector in your own catch blocks to log the fault, or to decide on a fallback.

catch (SqlException ex)
    when (SqlTransientFaultDetector.IsTransient(ex))
{
    log.Warn(ex, "Transient SQL fault, will retry");
}
Best for: custom policies
Around the database

The unglamorous parts, handled.

Resilience is only half of the data story. The same family covers the code that sits next to your queries: reading rows without DBNull surprises, building parameters, exporting tables, and seeing what a process is doing.

Safe row values

Typed extraction from DataRow and IDataReader that does not throw on DBNull or a missing column.

DataRowConverter.GetValue<T>
SQL parameters

Build SqlParameter values with types and directions, or turn an anonymous object into a parameter array.

SqlParameterBridge.ToSqlParameters
CSV export

Export a DataTable with quoting for delimiters and quotes, and a guard that neutralises spreadsheet formula injection in text cells.

DataTable.ToCsv
Chainable tracing

Drop .Trace("label") into a method chain to see a value pass through. One switch turns it off, and the sink is pluggable.

DiagnosticExtensions.Trace
Process metrics

CPU, private memory, thread count and optional GPU VRAM in one call. Anything unavailable on the platform comes back as -1 instead of throwing.

DiagnosticsProbe.GetProcessMetrics
Large file streaming

Process huge files line by line, or copy with progress, without loading everything into memory.

StreamExtensions.ForEachLine
Transformations

Install once.
Stop losing writes.

NuGet — take only what you need
$ dotnet add package Transformations.Dapper
$ dotnet add package Transformations.EntityFramework
$ dotnet add package Transformations
View on NuGet.org .NET 8 · .NET 9 · .NET 10 · MIT licensed