Case Study
Multi-Tenant ISV Platform Modernisation
Modernising a legacy .NET monolith to .NET 8 on Azure App Service — with full Azure DevOps CI/CD, zero-downtime deployment slot swapping, and production-safe rollback across six live instances.
The Challenge
Legacy Code, Risky Deployments
A multi-tenant independent software vendor ran a centralised API platform serving dozens of distributor tenants — B2B ERP workflows, branded B2C microsite storefronts, payment processing, and desktop sync clients — from a single legacy .NET codebase on Azure App Service.
The platform had grown to 400+ API endpoints but was still carrying patterns from its .NET Framework origins: legacy package references, ad-hoc HttpClient instantiation causing socket exhaustion under load, and no build-time guardrails against async deadlocks. The Azure App Service runtime stack had not been consistently configured for the modernised target framework.
Deployments were manual and risky. Six production App Service instances sat behind public load-balanced domains, yet there was no blue-green process, no automated health verification after release, and no way to roll back cleanly if one instance updated while another failed. SQL schema changes ran separately from application deploys. Production releases required confidence the entire fleet would stay on the same version.
Codebase Work
.NET 8 Runtime & Reliability Fixes
Before pipelines could be trusted in production, the application itself needed to run correctly on the modern stack and behave reliably under tenant load.
Runtime stack upgrade
The API was brought onto .NET 8 with the ASP.NET Core minimal hosting model. Every deploy now enforces the correct Azure App Service runtime stack (DOTNETCORE|8.0) on both the staging slot and production — verified again immediately after each slot swap so a misconfigured instance cannot reach live traffic.
Tenancy remains unchanged at the architecture level: one shared application resolves the current tenant from JWT claims or public microsite request keys, then routes to that tenant’s isolated SQL database at runtime.
Bug fixes & hardening
- Replaced
new HttpClient()anti-patterns across payment and integration flows with IHttpClientFactory, eliminating socket exhaustion and stale DNS under concurrent tenant traffic. - Added build-time async safety analyzers that fail the build on blocking calls (
.Result,.Wait()) andasync voidhandlers. - Centralised exception handling middleware for consistent JSON error responses and per-tenant DB logging.
- Lightweight
/healthendpoint with no database dependency — returns a build checksum the pipeline compares against the artifact so probes confirm the correct bits are live, not just that something responded.
DevOps
Azure DevOps Pipeline Architecture
We built four separate environment pipelines with reusable YAML templates — each pipeline scoped to its own Azure service connection and resources, following least-privilege branch promotion from development through to production.
Four pipelines, one promotion path
- Development — build, unit tests, and security scan only; no deployment
- Testing — deploys to two App Service instances with full slot-swap flow
- Staging — same pattern with enhanced security scanning before production
- Production — six App Service instances across three public API surfaces, manual approval gates, and blocks on critical vulnerability findings
Fifteen reusable templates cover build, Trivy and .NET package vulnerability scanning, slot preparation, deploy, sequential swap, CORS configuration, health checks with build checksum verification, and rollback. SQL migrations run through a separate pipeline family with versioned scripts and migration history tracking.
Branch promotion: dev → test → stage → prod (PR-only, no direct commits). Below is how a single production release flows through the pipeline.
View detailed pipeline steps & rollback logic
Health Checks
Verification at Every Stage
Health and checksum verification are dedicated pipeline phases — not folded into deploy or swap. Staging is verified before any traffic moves; production is re-verified after every swap.
Application endpoint
A dedicated GET /health controller returns HTTP 200 with a plain-text liveness message and a checksum of the deployed API build. It deliberately avoids touching tenant databases so a single slow tenant cannot fail a deployment probe. The pipeline reads that checksum in a separate verify phase and asserts it matches the value baked into the build artifact — catching wrong builds, partial deploys, or stale slots even when the endpoint returns 200.
Pipeline-driven checks
- Phase 2 — staging verify — dedicated parallel phase after all deploys complete. Each instance gets
GET /healthon its staging slot; the pipeline fails if the API checksum does not match the build. No swap starts until every instance passes. - Phase 4 — production verify — dedicated step after each sequential swap. Queries Azure
availabilityState, calls/healthon the live production slot, and re-verifies the checksum before the next app swaps. - Phase 5 — fleet check — environment-specific JSON config lists every public endpoint across all three API surfaces; each is probed 10 times with timeout, status code, and checksum validation.
- Configs in artifact — health and CORS JSON files ship with the build so the same artifact self-describes the expected checksum and how it should be verified after deploy.
Technology
What We Used
Application
Platform & DevOps
The Result
Safe, Repeatable Production Releases
The ISV can promote code through four environments with automated gates, deploy to six production instances without downtime, and recover cleanly when something goes wrong.
Similar Challenge?
Need Safer Deployments?
Whether you’re modernising a legacy .NET platform, standing up Azure DevOps from scratch, or need zero-downtime releases across multiple App Service instances — we design pipeline architecture that fits how your product actually runs in production.