Chapter 19 · SQL Server and Azure SQL Provider Deep Dive

Execution Strategies, Connection Resiliency, Azure SQL Transient Faults, and Retry Semantics

Configure SQL Server and Azure SQL retry strategies without confusing transient infrastructure faults with business concurrency conflicts, and make retry scope, transaction delegates, idempotency, and ambiguous commit outcomes explicit.

Advanced160–210 minutesretry + transaction-delegate labEF Core 10.0.11 · SQL Server provider 10.0.11 · .NET 10.0.11 · SDK 10.0.400SQL Server 2025 Developer free local path · Azure SQL optionalProvider deep-dive reviewed: August 2026

Learning outcomes

01

Explain what a SQL Server execution strategy retries and how transient error classification differs from optimistic concurrency.

02

Configure EnableRetryOnFailure for SQL Server and understand the Azure SQL UseAzureSql resiliency defaults.

03

Run explicit transactions through the execution-strategy delegate instead of creating an unretryable transaction scope.

04

Design idempotent operations and client-generated identifiers for ambiguous-commit scenarios.

05

Simulate one provider-classified transient failure in a disposable SQL Server lab and inspect retry logs.

06

Reject retries for permanent schema, authorization, validation, or deterministic query failures.

1. The problem: a cloud database can be healthy and still reject one attempt

Azure SQL and remote SQL Server deployments can experience brief connection resets, throttling, failovers, or other transient conditions. Retrying can improve availability only if the error is actually transient and replaying the operation is safe. A DbUpdateConcurrencyException caused by a rowversion mismatch is not transient infrastructure; it is a business-state conflict from Chapter 13.

Failure Typical policy Why
Azure SQL transient SqlException Execution strategy may retry Condition may disappear on a later attempt
DbUpdateConcurrencyException Merge/store-wins/client-wins/business policy Database state has legitimately changed
Invalid object/column Fix deployment/schema Retry repeats deterministic failure
Login denied Fix identity/permissions Retrying bad credentials increases noise
Unique constraint violation Domain/idempotency decision The write conflicts with durable data

2. Enable the SQL Server execution strategy intentionally

EnableRetryOnFailure configures SQL Server's provider execution strategy with known transient error numbers and optional additional ones. Do not copy retry counts/delays as universal tuning. Start from the provider defaults, observe the environment, and coordinate with request/job deadlines.

csharp · SQL Server configuration
services.AddDbContext<SqlServerServiceHubContext>(options =>    options.UseSqlServer(connectionString, sql =>        sql.EnableRetryOnFailure()));
csharp · Azure SQL provider configuration
services.AddDbContext<SqlServerServiceHubContext>(options =>    options.UseAzureSql(azureSqlConnectionString));

Current EF guidance configures appropriate resiliency automatically with UseAzureSql/UseAzureSynapse. With ordinary UseSqlServer, enable retries deliberately when the deployment needs them.

3. A retrying strategy changes the unit of replay

When retries are enabled, each EF query and each SaveChanges call is normally an independently retriable unit. Starting a user transaction outside the execution strategy creates a unit EF cannot safely replay by itself. EF therefore requires the whole transaction block to execute inside CreateExecutionStrategy().ExecuteAsync.

csharp · wrong: transaction opened outside the retry delegate
await using var tx = await db.Database.BeginTransactionAsync(ct);await db.SaveChangesAsync(ct); // can throw when retry strategy is enabledawait tx.CommitAsync(ct);
csharp · repair: replay the complete transaction unit
var strategy = db.Database.CreateExecutionStrategy();await strategy.ExecuteAsync(async () =>{    await using var db = factory.CreateDbContext();    await using var tx = await db.Database.BeginTransactionAsync(ct);    // all commands that must commit together are inside this delegate    await ApplyServiceHubChangesAsync(db, ct);    await db.SaveChangesAsync(ct);    await tx.CommitAsync(ct);});

The delegate may run more than once. Anything inside it—including custom SQL, side effects, and ID allocation—must be replay-safe or explicitly guarded.

4. Controlled retry lab: add one fake SQL error number, then fail once

For a deterministic local demonstration, configure a user-defined SQL error number as transient only in the disposable lab. A small table stores a fail-once marker. The first execution deletes the marker and throws error 50001; the provider execution strategy sees 50001 in the added transient list and retries; the second execution succeeds.

csharp · lab-only strategy classification
options.UseSqlServer(connectionString, sql =>    sql.EnableRetryOnFailure(        maxRetryCount: 3,        maxRetryDelay: TimeSpan.FromSeconds(2),        errorNumbersToAdd: new[] { 50001 }));
sql · lab SQL: fail the first attempt only
IF OBJECT_ID(N'dbo.RetryLab', N'U') IS NULL    CREATE TABLE dbo.RetryLab (Id int NOT NULL PRIMARY KEY);IF NOT EXISTS (SELECT 1 FROM dbo.RetryLab WHERE Id = 1)    INSERT dbo.RetryLab(Id) VALUES (1);-- Run the following inside the execution-strategy delegate:IF EXISTS (SELECT 1 FROM dbo.RetryLab WHERE Id = 1)BEGIN    DELETE dbo.RetryLab WHERE Id = 1;    THROW 50001, 'ServiceHub retry lab transient fault', 1;END;SELECT 1;
csharp · execute provider command through the EF strategy
var strategy = db.Database.CreateExecutionStrategy();await strategy.ExecuteAsync(async () =>{    var connection = db.Database.GetDbConnection();    if (connection.State != ConnectionState.Open)        await db.Database.OpenConnectionAsync(ct);    await using var command = connection.CreateCommand();    command.CommandText = labSql;    await command.ExecuteNonQueryAsync(ct);});
Never add arbitrary production errors to the transient list

The lab proves mechanics, not that error 50001 should be retried in production. Production error classification must come from documented SQL Server/Azure SQL behavior and observed failure modes.

5. Ambiguous commit is different from a failed command before commit

If the connection drops while a transaction is being committed, the client may not know whether the database committed. Blindly retrying an INSERT with a store-generated identity can create a duplicate logical operation. Idempotency therefore needs a durable application key or a verification step.

csharp · client-generated operation identity
public sealed class DispatchCommand{    public Guid OperationId { get; init; } = Guid.NewGuid();    public required string WorkOrderNumber { get; init; }}// Persist OperationId under a UNIQUE constraint in the same local transaction.// A replay can detect the already-committed operation instead of duplicating it.

For ServiceHub, the transactional outbox introduced earlier is a stronger boundary than “send message then retry DB write.” Database state and outbox record commit together; message delivery is replayable/idempotent later.

6. Retries and transactions increase memory/time cost

Execution strategies may buffer result sets so a query can be replayed, increasing memory for large queries. Retries also extend tail latency. Combine Chapter 17 result bounding/projection with retry configuration, and include retry count/delay in request or job SLO analysis.

Observability

Log retry attempt, provider error number, delay, operation name, trace ID, and final outcome. Do not log connection passwords or parameter payloads. Correlate Azure SQL metrics/failover events with EF retry telemetry before tuning.

7. Failure case: retrying a concurrency conflict

A generic “retry on any exception” policy catches DbUpdateConcurrencyException and immediately calls SaveChanges again with the same original rowversion. Every attempt correctly affects zero rows. The loop adds load but never resolves the changed business state.

Repair: handle concurrency conflicts with the domain-specific merge/store-wins/client-wins workflow from Chapter 13. Keep provider transient retries at the connection/execution-strategy layer. The two policies may both exist in one application, but they answer different questions.

8. Mandatory lab: distinguish retryable from permanent

  1. Use a disposable SQL Server 2025 Developer database and configure the lab-only additional transient error 50001.
  2. Subscribe to normal EF logging and capture execution-strategy retry events without sensitive parameter values.
  3. Create/reset dbo.RetryLab, run the fail-once delegate, and verify one failure followed by success.
  4. Run a deliberately invalid SQL query such as selecting a nonexistent column and verify it is not made successful by the retry policy.
  5. Reproduce the Chapter 13 rowversion conflict and prove it surfaces as DbUpdateConcurrencyException, not as a provider transient retry.
  6. Wrap two dependent writes in an explicit transaction inside CreateExecutionStrategy().ExecuteAsync and verify both commit together.
  7. Record retry count, elapsed time, SQL error number, transaction scope, and final database state.

9. Production judgment and bridge

Enable connection resiliency for environments that actually exhibit transient SQL Server/Azure SQL failures, but keep retry scope bounded and observable. Never assume retries make non-idempotent side effects safe. Run user transactions inside the strategy delegate, use durable operation IDs where commit ambiguity matters, and keep business concurrency policy separate. The next lesson turns from runtime resilience to SQL Server physical design: indexes and migration DDL.

Check your understanding

  1. Is DbUpdateConcurrencyException a transient SQL Server fault?
  2. Why must a user transaction run inside the execution-strategy delegate?
  3. What can make an ambiguous commit dangerous with identity keys?
  4. Why is a client-generated operation ID useful?
  5. Should maxRetryCount be copied from a tutorial into every service?
  6. What does the fail-once 50001 lab prove?
Review the answers

1. No. It represents a business-state concurrency conflict detected through a zero-row concurrency predicate.

2. Because the strategy must be able to replay the complete atomic unit if a transient failure occurs.

3. The database may have committed even though the client did not receive confirmation; replay can insert a duplicate logical operation.

4. It gives a stable idempotency key that can be constrained and checked across retries.

5. No. Retry policy must fit provider guidance, observed faults and the operation/request latency budget.

6. Execution-strategy mechanics and classification extensibility only; it does not make 50001 a valid production transient error.

Authoritative references

Retry behavior is provider- and topology-specific. Re-check SQL Server/Azure SQL guidance and your incident data before changing production policy.

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.

\n