Production Readiness — Azure SQL, Connection Resilience and Pre-Deployment Checklist

The final lesson brings together the complete SQL Server knowledge from Part 6 into a production readiness checklist for the BlogApp. Moving from LocalDB development to Azure SQL production involves connection string changes, firewall configuration, Managed Identity authentication, and performance validation. The checklist ensures nothing is forgotten before go-live — from security settings to backup schedules to monitoring configuration.

Azure SQL and Production Checklist

// ── Azure SQL connection with Managed Identity (no password) ──────────────
// appsettings.Production.json:
// {
//   "ConnectionStrings": {
//     "Default": "Server=blogapp.database.windows.net;Database=BlogApp;
//                 Authentication=Active Directory Default;Encrypt=True;
//                 TrustServerCertificate=False;Application Name=BlogApp"
//   }
// }

// Program.cs — configure for Azure SQL resilience:
builder.Services.AddDbContext<AppDbContext>((sp, opts) => {
    opts.UseSqlServer(
        builder.Configuration.GetConnectionString("Default"),
        sqlOpts => {
            sqlOpts.EnableRetryOnFailure(
                maxRetryCount:    5,
                maxRetryDelay:    TimeSpan.FromSeconds(30),
                errorNumbersToAdd: null  // includes all transient Azure SQL errors
            );
            sqlOpts.CommandTimeout(30);
        });
    opts.AddInterceptors(
        sp.GetRequiredService<AuditInterceptor>(),
        sp.GetRequiredService<SoftDeleteInterceptor>()
    );
});

// ── Health check — database connectivity probe ────────────────────────────
builder.Services.AddHealthChecks()
    .AddDbContextCheck<AppDbContext>("database",
        failureStatus: HealthStatus.Degraded,
        tags: ["db", "sql"]);

app.MapHealthChecks("/health/db", new HealthCheckOptions {
    Predicate = check => check.Tags.Contains("db"),
});
// Kubernetes readiness probe: GET /health/db → 200 OK when DB reachable
-- ════════════════════════════════════════════════════════════════════════════
-- BLOGAPP PRODUCTION READINESS CHECKLIST
-- ════════════════════════════════════════════════════════════════════════════

-- ── SECURITY ──────────────────────────────────────────────────────────────
-- [ ] sa account disabled:
SELECT name, is_disabled FROM sys.server_principals WHERE name = 'sa';

-- [ ] Application login uses least privilege (no db_owner, no sysadmin):
SELECT dp.name, r.name AS Role
FROM   sys.database_role_members m
JOIN   sys.database_principals dp ON dp.principal_id = m.member_principal_id
JOIN   sys.database_principals r  ON r.principal_id  = m.role_principal_id
WHERE  dp.name = 'blogapp_api';

-- [ ] No passwords in appsettings — using Managed Identity or Key Vault
-- [ ] TrustServerCertificate=False in production connection string

-- ── PERFORMANCE ───────────────────────────────────────────────────────────
-- [ ] RCSI enabled:
SELECT is_read_committed_snapshot_on FROM sys.databases WHERE name = 'BlogApp';
-- Should return 1

-- [ ] Query Store enabled:
SELECT actual_state_desc FROM sys.database_query_store_options;
-- Should return 'READ_WRITE'

-- [ ] All production indexes in place:
SELECT i.name, i.type_desc, STATS_DATE(i.object_id, i.index_id) AS LastStatsUpdate
FROM sys.indexes i JOIN sys.tables t ON t.object_id = i.object_id
WHERE t.name IN ('Posts', 'Users', 'Tags', 'PostTags', 'Comments')
ORDER BY t.name, i.name;

-- [ ] Filtered index for published posts:
SELECT name FROM sys.indexes WHERE name = 'IX_Posts_Published_Date';

-- ── RELIABILITY ───────────────────────────────────────────────────────────
-- [ ] FULL recovery model (for log backups + point-in-time recovery):
SELECT recovery_model_desc FROM sys.databases WHERE name = 'BlogApp';
-- Should return 'FULL'

-- [ ] AUTO_SHRINK OFF:
SELECT is_auto_shrink_on FROM sys.databases WHERE name = 'BlogApp';
-- Should return 0

-- [ ] AUTO_CLOSE OFF:
SELECT is_auto_close_on FROM sys.databases WHERE name = 'BlogApp';
-- Should return 0

-- [ ] Backup jobs configured and last backup recent:
SELECT database_name, backup_type = CASE type WHEN 'D' THEN 'Full'
    WHEN 'I' THEN 'Differential' WHEN 'L' THEN 'Log' END,
    backup_finish_date
FROM msdb.dbo.backupset
WHERE database_name = 'BlogApp'
ORDER BY backup_finish_date DESC;

-- ── MAINTENANCE ───────────────────────────────────────────────────────────
-- [ ] DBCC CHECKDB scheduled (no integrity errors):
-- Check CommandLog table (Ola Hallengren) or SQL Server error log

-- [ ] Index maintenance job configured:
SELECT name, enabled, next_run_date, next_run_time FROM msdb.dbo.sysjobs
WHERE name LIKE '%Index%';

-- ── MONITORING ────────────────────────────────────────────────────────────
-- [ ] Alerts configured for severity 17+ errors
-- [ ] Disk space monitoring in place
-- [ ] PLE baseline established
-- [ ] Connection string Application Name set (for profiling attribution)
Note: Azure SQL Database (PaaS) handles many operational concerns automatically: high availability with 99.99% SLA, automatic backups (up to 35 days point-in-time restore), automatic patching, and built-in geo-replication. The trade-off: Azure SQL does not support all SQL Server features (SQL Server Agent, linked servers, some DBCC commands). Azure SQL Managed Instance provides near-100% SQL Server compatibility as a managed service — ideal for migrating existing on-premises SQL Server applications to Azure without code changes.
Tip: Configure EF Core’s retry-on-failure with a higher retry count and delay for Azure SQL than for on-premises SQL Server. Azure SQL has occasional transient connectivity issues (connections dropped during load balancing, brief unavailability during automatic failover) that are not present in on-premises SQL Server. Azure SQL returns specific transient error codes (40197, 40501, 40613, 49918, 49919, 49920) — EF Core’s built-in retry strategy handles all of these. Retry 5 times with exponential backoff up to 30 seconds covers the vast majority of transient failures.
Warning: Never use SQL Server’s COMPATIBILITY_LEVEL below the current SQL Server version in production. Some teams set compatibility level to an older version to avoid re-testing query behaviour after upgrades — but this disables query optimizer improvements that could help performance. Test queries against the current compatibility level in staging, fix any behavioural differences, and run production at the current level to benefit from all optimiser improvements.

🎓 Part 6 Complete — SQL Server Summary

Part 6 has covered SQL Server from setup through production — 10 chapters, 50 lessons, progressing from LocalDB installation to Azure SQL deployment with security, performance, and reliability best practices. The BlogApp now has a complete, production-ready database layer: a well-designed schema, correct indexes, optimised queries, proper concurrency handling, and an EF Core integration that uses advanced features correctly.

The next part — Part 7: Full Stack Integration — connects the Angular frontend to the ASP.NET Core API, implementing real end-to-end features with the complete C# + Angular + SQL Server stack working together.

Common Mistakes

Mistake 1 — Not enabling RCSI before go-live (read/write blocking under load)

❌ Wrong — default READ COMMITTED without RCSI; first load test reveals massive blocking between reads and writes.

✅ Correct — include ALTER DATABASE BlogApp SET READ_COMMITTED_SNAPSHOT ON in the production deployment script.

Mistake 2 — No health check endpoint for load balancer / Kubernetes probes

❌ Wrong — no health endpoint; load balancer routes traffic to unhealthy pods that cannot connect to the database.

✅ Correct — MapHealthChecks("/health/db") with database check; Kubernetes readiness probe uses this endpoint.

🧠 Test Yourself

The BlogApp is deployed to Azure App Service connecting to Azure SQL with Managed Identity. The connection string has no username or password. How does authentication work?