Learnixo
Back to blog
AI Systemsintermediate

Azure SQL Database — Production Configuration for .NET Applications

Configure and use Azure SQL Database in .NET applications: connection resilience, Managed Identity authentication, geo-replication, elastic pools, and performance monitoring with Query Performance Insight.

Asma Hafeez KhanMay 16, 20265 min read
AzureAzure SQLSQL Server.NETDatabase
Share:𝕏

Azure SQL vs SQL Server

Azure SQL Database:
  → Fully managed PaaS: no patching, no backups configuration, no failover setup
  → Automatic backups: point-in-time restore up to 35 days
  → Built-in high availability: 99.99% SLA on General Purpose tier
  → Auto-scaling: serverless tier scales to zero when idle
  → Active Geo-Replication: read replicas in other regions

SQL Server on Azure VM (IaaS):
  → Full control over SQL Server version, features, and configuration
  → Required for: SQL Server Agent jobs, linked servers, CLR assemblies
  → More operational overhead: you manage patching, backups, clustering
  → Choose when Azure SQL Database lacks a feature you depend on

Connection Resilience with EF Core

C#
// Azure SQL may have transient connection errors (failover, connection pool churn)
// EF Core built-in retry strategy handles these automatically

builder.Services.AddDbContext<ClinicalDbContext>(options =>
{
    options.UseSqlServer(
        builder.Configuration.GetConnectionString("Clinical"),
        sqlOptions =>
        {
            // Retry up to 5 times for transient Azure SQL errors
            sqlOptions.EnableRetryOnFailure(
                maxRetryCount:       5,
                maxRetryDelay:       TimeSpan.FromSeconds(30),
                errorNumbersToAdd:   null);

            // Command timeout: longer than default for complex clinical reports
            sqlOptions.CommandTimeout(60);
        });
});

Managed Identity Connection

C#
// No username/password in the connection string
// Azure App Service uses its Managed Identity to authenticate

// Connection string:
// "Server=tcp:clinical-sql.database.windows.net,1433;
//  Initial Catalog=ClinicalDb;
//  Authentication=Active Directory Managed Identity"

// For local development: use Azure CLI or Visual Studio credentials
// DefaultAzureCredential picks up whatever is available:
// → Managed Identity in production (App Service)
// → Azure CLI credentials locally (az login)
// → Visual Studio / VS Code credentials locally

// If using Microsoft.Data.SqlClient directly (not via EF):
var credential = new DefaultAzureCredential();
var token      = await credential.GetTokenAsync(
    new TokenRequestContext(new[] { "https://database.windows.net/.default" }), ct);

connection.AccessToken = token.Token;
await connection.OpenAsync(ct);

// EF Core handles this automatically when connection string includes
// Authentication=Active Directory Managed Identity

Connection Pooling and Performance

C#
// Azure SQL has per-database connection limits
// Tier:       Max connections
// Basic:      30
// Standard S1: 60
// General Purpose 4 vCores: 408

// Without connection pooling: every request opens and closes a TCP connection
// → Expensive, slow, hits connection limits immediately

// EF Core uses ADO.NET connection pooling by default — no extra configuration needed
// Each DbContext gets a connection from the pool; returns it on Dispose

// For high-concurrency clinical apps: tune the pool size
var connectionString = builder.Configuration.GetConnectionString("Clinical");
var connStr = new SqlConnectionStringBuilder(connectionString)
{
    MaxPoolSize = 100,   // per application instance
    MinPoolSize = 5,     // keep 5 warm connections ready
    ConnectTimeout = 15  // seconds before timeout
}.ToString();

// Monitor connection pool saturation via Azure Monitor:
// Metric: "Connection failed" — indicates pool exhaustion
// Metric: "Connections used" — alert if above 80% of the tier limit

Elastic Pools for Multi-Tenant Applications

Scenario: Clinical platform deployed to 20 hospital trusts.
  → Each trust has its own database (data isolation)
  → Most trusts have low utilisation during the day
  → Peak hours vary (different time zones, shift patterns)

Problem with 20 separate databases:
  → 20 × General Purpose S2 = 20 × £X/month even when idle
  → Each database is provisioned for peak, not average

Elastic Pool:
  → Pool of DTUs/vCores shared across databases
  → Databases burst to pool capacity when needed
  → Idle databases use near-zero resources from the pool
  → Typically 30-50% cost saving for SaaS/multi-tenant workloads

Azure Bicep:
  resource elasticPool 'Microsoft.Sql/servers/elasticPools@2022-05-01-preview' = {
    name: 'clinical-elastic-pool'
    parent: sqlServer
    sku: { name: 'GP_Gen5', tier: 'GeneralPurpose', capacity: 8 }
    properties: {
      maxSizeBytes: 107374182400 // 100 GB shared across all databases
      perDatabaseSettings: { minCapacity: 0, maxCapacity: 4 }
    }
  }

Active Geo-Replication for Read Replicas

C#
// Geo-replication: primary in UK South, read replica in UK West
// Clinical reporting queries run against the replica — no load on primary

// Register two DbContexts: one for writes, one for reads
builder.Services.AddDbContext<ClinicalWriteDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("ClinicalPrimary")));

builder.Services.AddDbContext<ClinicalReadDbContext>(options =>
    options.UseSqlServer(
        builder.Configuration.GetConnectionString("ClinicalReplica"),
        sql => sql.EnableRetryOnFailure(3, TimeSpan.FromSeconds(5), null)));

// Query handlers: inject the read context for queries
public sealed class GetPrescriptionListHandler
    : IRequestHandler<GetPrescriptionListQuery, PagedList<PrescriptionDto>>
{
    private readonly ClinicalReadDbContext _db; // read replica

    public async Task<PagedList<PrescriptionDto>> Handle(
        GetPrescriptionListQuery query, CancellationToken ct)
    {
        return await _db.Prescriptions
            .AsNoTracking()
            .Where(p => p.WardId == query.WardId)
            .ProjectTo<PrescriptionDto>(_mapper.ConfigurationProvider)
            .ToPagedListAsync(query.Page, query.PageSize, ct);
    }
}

// Replica lag: typically under 2 seconds — acceptable for reporting
// Not acceptable for: prescription approval confirmation (use primary for those reads)

Production issue I've seen: A clinical platform on Azure SQL Standard S1 (60 max connections) was running at peak with 8 API instances, each with a default max pool size of 100. Mathematical maximum concurrent connections: 800. The database tier supported 60. Under ward shift-change load, connections exhausted and the application returned "connection pool timeout" errors. Nurses could not access prescriptions for 20 minutes. The fix: set MaxPoolSize = 7 per instance (7 × 8 = 56, safely under the 60-connection limit) and upgrade to S2 for headroom. Monitoring connection pool utilisation from day one would have caught this before production.


Key Takeaway

Enable EF Core's built-in retry policy for Azure SQL transient errors. Use Managed Identity — no credentials in connection strings. Set MaxPoolSize in the connection string to stay within your tier's connection limit. Use Elastic Pools for multi-tenant deployments where databases have variable utilisation. Configure Active Geo-Replication for read replicas to offload reporting queries from the primary. Monitor connection pool utilisation and DTU/vCore consumption via Azure Monitor before issues become incidents.

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.