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.
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.
| Retry | Wait range | Relative |
|---|
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.
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.
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
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.
Typed extraction from DataRow and IDataReader that does not throw on DBNull or a missing column.
Build SqlParameter values with types and directions, or turn an anonymous object into a parameter array.
Export a DataTable with quoting for delimiters and quotes, and a guard that neutralises spreadsheet formula injection in text cells.
Drop .Trace("label") into a method chain to see a value pass through. One switch turns it off, and the sink is pluggable.
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.GetProcessMetricsProcess huge files line by line, or copy with progress, without loading everything into memory.
StreamExtensions.ForEachLine