Skip to content
Raghuraja — home
Reference architecture

Modernizing a large .NET portfolio without losing control

A reference architecture for moving hundreds of .NET Framework applications forward: evidence-based assessment, per-application paths, dependency-aware waves, and release gates you can defend.

8 min read · Application modernization

The problem

A large enterprise runs a portfolio of a few hundred .NET applications that accumulated over fifteen or more years. Most target .NET Framework 4.x. Some are ASP.NET Web Forms or MVC sites on IIS, some are WCF services, some are Windows services and scheduled console jobs, and a meaningful number are WinForms or WPF desktop tools. They share internal NuGet packages, a handful of common databases, and a lot of undocumented configuration.

The business wants to move this estate onto a supported modern runtime and a container platform. The real goal is not "be on .NET 10". It is: reduce operational and security risk, make the estate cheaper to change, and do it without a long freeze on feature delivery or a wave of production incidents.

The hard part is control. With hundreds of applications, a team can easily spend months migrating code before discovering that a shared library, a database, or a release process forces a different order — or that nobody can say which migrated applications are actually safe to release.

Illustrative scale and assumptions

DimensionAssumption used in this design
Applications~300 deployable units across ~40 business domains
SourceSeveral hundred repositories, mixed Git and migrated TFVC history
Shared code50–100 internal NuGet packages, some multi-targeted, some not
Runtime todayMostly .NET Framework 4.6–4.8, IIS on Windows Server, some Windows services
TargetCurrent LTS .NET on Linux containers where viable; Windows hosting where not
PlatformA managed Kubernetes or OpenShift platform, plus a smaller Windows estate
ConstraintFeature teams keep shipping during the program

These numbers are assumptions for reasoning, not measurements.

Constraints and non-functional requirements

  • Behavior preservation is the primary quality attribute. Modernization that changes business behavior is a defect, even when the new behavior looks "better".
  • No big-bang cutover. Each application must be releasable independently, with rollback.
  • Shared dependencies set the order. A shared library cannot move to net10.0 alone until its consumers can handle it; multi-targeting is the bridge.
  • Security and compliance stay continuous. Identity, secrets handling and audit logging must be at least as strong after migration as before.
  • Observability before migration, not after. Without a baseline of today's behavior and performance, "validated" means nothing.
  • Cost must be visible per wave, so the program can be re-planned with evidence instead of optimism.

End-to-end approach

  1. 01.

    Assess

    Automated inventory of every repo: target frameworks, project style, packages, APIs used, hosting model, data access, config.

  2. 02.

    Classify

    Assign each app a modernization path and a difficulty rating from evidence, not interviews alone.

  3. 03.

    Plan waves

    Order work by the dependency graph: shared libraries first, leaf applications last.

  4. 04.

    Define gates

    Agree the evidence each app must produce before release: tests, parity checks, performance, security.

  5. 05.

    Migrate

    Repeatable playbooks per path, applied by domain teams with a central enablement team.

  6. 06.

    Validate

    Side-by-side comparison against the baseline; progressive rollout with rollback.

  7. 07.

    Operate

    Retire old hosting, keep the platform current, feed lessons back into the playbooks.

Program pipeline. Each stage produces an artifact the next stage consumes: an inventory, a classification, a wave plan, gate definitions, migrated code, validation evidence and an operating baseline.

1. Assess: build the inventory from the code

Interviews and spreadsheets go stale on day one. The assessment is a pipeline that scans every repository and writes structured facts:

  • Project format (legacy .csproj vs SDK-style), target frameworks, and output types.
  • Package references with versions, flagged where a package has no compatible modern release.
  • Use of APIs that do not exist on modern .NET: System.Web, WCF server-side hosting, .NET Remoting, AppDomain creation, Windows Workflow Foundation, and code-access security.
  • Hosting model: IIS site, Windows service, scheduled task, desktop app.
  • Data access: Entity Framework 6, LINQ to SQL, raw ADO.NET, stored-procedure heavy designs.
  • Configuration: web.config/app.config transforms, machine-level config, registry and file-system dependencies.
  • Inbound and outbound dependencies: which applications call which services, which share databases or queues.

The output is a queryable dataset, not a report. Every later decision cites it.

2. Classify: one path per application

PathWhen it fitsTypical work
Retain / retireLow value, low change rate, or already scheduled for replacementIsolate, patch, document the exit
RehostMust keep .NET Framework for now (heavy Web Forms, WF, third-party constraints)Move to supported Windows hosting; containerize on Windows only if the platform supports it well
Upgrade in placeLibraries, console jobs, services, MVC/Web API apps with modest System.Web couplingConvert to SDK-style, retarget, replace incompatible packages, port hosting to the generic host / ASP.NET Core
Incremental migrationLarge ASP.NET apps that cannot move in one releasePut ASP.NET Core in front with a reverse proxy, route endpoints across one at a time, share session/auth during the transition
Re-architectOnly where the domain needs a different shape (scaling, ownership, release independence)Decompose behind stable contracts; justify per case
ReplaceCommodity capability now better served by a product or platform serviceData migration and integration, not code migration

The classification uses explicit rules over the inventory — for example, "Web Forms pages above a threshold → rehost or incremental, never upgrade in place". A human review confirms or overrides each result, and the reason is recorded.

3. Plan waves around the dependency graph

Shared code is the critical path. The wave planner builds a graph of project and package references and sorts it:

  1. Foundation wave: shared NuGet packages multi-target net48;net10.0 (or an intermediate netstandard2.0 where that is cheaper). Nothing breaks for existing consumers.
  2. Pilot wave: five to ten applications that cover every migration path, chosen for representativeness rather than ease. The playbooks are tuned here.
  3. Domain waves: applications grouped by domain and by shared database, so data-layer changes are made once per group.
  4. Long tail: low-change applications, often rehosted or retired rather than upgraded.

Each wave has an explicit entry condition (its dependencies are migrated) and exit condition (all applications passed their gates or were re-planned).

4. Quality gates that make release decisions defensible

A migrated application is releasable only when it produces this evidence:

GateEvidence
BuildClean build on the target framework, analyzers enabled, no suppressed compatibility warnings without a recorded reason
TestsExisting tests pass. Where coverage is thin, characterization tests are written before migration against the old system
Behavioral parityRecorded production-like requests replayed against old and new; responses compared with known-acceptable differences listed
PerformanceLatency and throughput within an agreed tolerance of the baseline under representative load
SecurityDependency scan clean or risk-accepted; identity flows verified; secrets moved out of config files
OperabilityHealth checks, structured logs, metrics and traces wired to the platform; runbook updated
RollbackA tested way back: the previous deployment still runnable, data changes backward-compatible

The gates are automated in CI/CD wherever possible. A release manager checks evidence — not a migration engineer's confidence.

Target architecture

  1. Edge

    • DNS & TLS
    • WAF
    • API gateway / reverse proxy
    • Incremental-migration routing
  2. Applications

    • ASP.NET Core services
    • Generic-host workers
    • Migrated APIs
    • Rehosted .NET Framework apps
  3. State & data

    • Existing SQL databases
    • Schema changes backward-compatible
    • Distributed cache
    • Message broker
  4. Security

    • Central identity provider (OIDC)
    • Workload identity
    • Secret store
    • Dependency & image scanning
  5. Observability

    • OpenTelemetry traces
    • Structured logs
    • Metrics & SLOs
    • Parity dashboards
  6. Delivery

    • Pipeline templates per path
    • Container image baseline
    • Progressive rollout
    • Automated gates
  7. Platform

    • Kubernetes / OpenShift (Linux)
    • Windows hosting for rehost path
    • Infrastructure as code
Layers of the target state, from the request edge to the platform. Applications that cannot leave .NET Framework yet run in the Windows hosting layer behind the same edge, observability and identity.

Key design decisions

Containers on Linux by default, Windows only by exception. Linux images are smaller, faster to start and better supported by most platforms. The exception list is explicit: anything still on .NET Framework, or with hard Windows dependencies (COM interop, certain printing or Office automation).

Incremental migration for large web applications. For a big ASP.NET application, a reverse proxy routes requests to either the legacy app or a new ASP.NET Core app, one route at a time. Microsoft documents this pattern and provides System.Web adapters to share session and authentication during the transition. This turns one risky cutover into many small, reversible ones.

Keep the database stable during code migration. Changing runtime and schema at the same time doubles the variables in every incident. Entity Framework 6 runs on modern .NET, so moving to EF Core is a separate, optional decision made per application.

Standardize the "paved road". Every migrated service starts from the same template: hosting, health checks, telemetry, configuration binding, authentication and a pipeline. The template is where platform standards are enforced, so teams don't re-decide them.

Alternatives considered

OptionWhy it was not the default
Rewrite applications on a new stackHighest behavior risk, longest time to value, and it discards tested business logic. Kept only for the re-architect path, justified per application.
Rehost everything on Windows VMs first, modernize laterLowers infrastructure risk but leaves runtime and security debt in place. Often becomes the permanent state. Used for the long tail, not as the strategy.
Big-bang migration per domainSimpler to plan on paper. In practice one blocked application stalls a whole domain, and rollback is all-or-nothing.
Decompose into microservices while migratingMixes two hard problems. Decomposition is a design decision driven by ownership and scaling needs, not a side effect of a runtime upgrade.

Technical risks and mitigations

RiskMitigation
Hidden behavior change (serialization defaults, culture/formatting, date handling, case sensitivity on Linux file systems)Characterization tests and replay-based parity checks before release; a catalogue of known differences maintained by the enablement team
Shared library blocks many appsMulti-targeting in the foundation wave; dependency graph drives the plan
Unsupported technology (WCF server, Web Forms, WF)Classified early into rehost, incremental or re-architect paths; community ports such as CoreWCF evaluated case by case, not assumed
Performance regression under real loadBaseline captured before migration; load test as a gate; progressive rollout with automatic rollback on SLO breach
Configuration and secrets driftConfiguration moved to environment-specific providers with a secret store; config differences diffed as part of the parity gate
Program fatigue: early waves go well, long tail stallsClassify the long tail early; retire or rehost deliberately instead of leaving it "for later"
Teams bypass the paved roadMake the template the fastest path; review exceptions, don't forbid them

Validation: what would show this is working

The program should define these measures up front and report them per wave:

  • Share of applications in each path, and how often classification was overridden (high override rates mean the rules are wrong).
  • Gate pass rate on first attempt, and which gate fails most.
  • Production incidents attributed to migration, per wave, compared with the pre-migration baseline.
  • Lead time from "wave started" to "all applications released or re-planned".
  • Infrastructure and licensing cost before and after, per application, where it can be measured honestly.

Without these, a modernization program can report activity — repositories touched, pull requests merged — while risk quietly stays where it was.

What I would do differently, and open trade-offs

  • Invest in characterization tests earlier than feels comfortable. It's tempting to start migrating code in week one. The applications with the weakest test coverage are exactly the ones where parity evidence matters most.
  • Treat the inventory as a product. If the scanner is a one-off script, it decays. Kept current, it becomes the source of truth for security, ownership and future upgrades — the next LTS move becomes much cheaper.
  • Unresolved trade-off: central team vs domain teams. A central team migrates faster at first; domain teams retain the knowledge. A hybrid — central enablement building playbooks and tooling, domain teams doing the work — is my default, but it needs strong product ownership to avoid stalling.
  • Unresolved trade-off: where AI fits. AI assistance can speed up repetitive code changes and summarize unfamiliar code. It shouldn't decide classification or release readiness without evidence — the approach in the evidence-first AI architecture applies here directly.

Next conversation

Have a difficult architecture problem? Let's talk.

I'm interested in technical architecture challenges where the decisions matter and the engineering has to hold up in production.

Get in touch