Most .NET Framework to .NET 8 migrations fail not because the technical work is too hard but because the approach is wrong. Teams treat the migration as a separate project that runs in parallel with the product, discover that "in parallel" means the migration gets deprioritized whenever a real feature ships, and find themselves twelve months in with a half-migrated codebase that is harder to work with than the original.
At Blackthorn Vision, a Microsoft-partnered AI and machine learning development helping enterprise teams build and modernize complex software products, we have migrated several enterprise .NET development services to .NET 8 while keeping those products in active delivery. This post covers the approach that makes this work, the specific failure modes it avoids, and the technical decisions that determine whether a migration stays on track.
Why the Big-Bang Approach Fails
The instinct when starting a .NET Framework 4.x to .NET 8 migration is to create a new solution, port everything across, and cut over when it is ready. This approach has obvious appeal: you start with a clean architecture, no legacy constraints, and the full benefit of modern .NET.
The problem is the timeline. A .NET Framework 4.x platform that has been running in production for eight to ten years has accumulated complexity that no initial estimate fully captures. SQL Server Agent jobs doing application logic. Windows Services nobody fully understands because the authors left. Business rules embedded in stored procedures. Direct database connections from downstream systems that do not appear in any architecture diagram. Every one of these is a dependency that has to be resolved before the migration can cut over, and they surface gradually rather than all at once.
A big-bang migration consistently produces the same outcome: the new system runs behind the legacy system in capability, the cutover date slips repeatedly, and eventually leadership loses confidence. The team either forces a cutover before the new system is ready or cancels the migration and starts managing the legacy system again.
Microsoft frequently recommends YARP-based incremental migration as one of the primary approaches for ASP.NET modernization scenarios precisely because Microsoft's incremental ASP.NET to ASP.NET Core migration guidance. Early functional extraction is the strongest predictor of whether a migration succeeds. The big-bang approach, by definition, produces zero functional extraction in the first 90 days, which is when the migration is most likely to be deprioritized in favor of product delivery.
The Strangler Fig Pattern: What It Actually Looks Like in .NET
The strangler fig pattern migrates the application incrementally by routing traffic gradually from the legacy system to new .NET 8 services, one component at a time. The legacy system continues to run and handle everything that has not yet been migrated. From the perspective of users and external systems, nothing changes.
The routing layer that makes this work in the .NET ecosystem is YARP (Yet Another Reverse Proxy), a Microsoft-developed reverse proxy library built on ASP.NET Core middleware. Microsoft frequently recommends YARP as one of the primary approaches for incremental ASP.NET to ASP.NET Core migration scenarios, with official guidance covering the setup in detail.
The setup is straightforward. You create a new ASP.NET Core project that hosts YARP. Initially, YARP forwards 100% of requests to the legacy .NET Framework application. As you migrate each component, you add routing rules that send specific routes to the new service instead of the legacy system:
// Initial YARP configuration, all traffic to legacy system
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
// appsettings.json
{
"ReverseProxy": {
"Routes": {
"catch-all": {
"ClusterId": "legacy-cluster",
"Match": { "Path": "{**catch-all}" }
},
"reports-route": {
"ClusterId": "new-net8-cluster",
"Match": { "Path": "/api/reports/{**remainder}" }
}
},
"Clusters": {
"legacy-cluster": {
"Destinations": {
"legacy": { "Address": "https://legacy-app.internal/" }
}
},
"new-net8-cluster": {
"Destinations": {
"new": { "Address": "https://new-reports-service.internal/" }
}
}
}
}
}
Both systems run in production simultaneously. The routing configuration is the only thing that changes as each component is migrated. Rolling back a component means updating one routing rule, not redeploying the entire application.
Before Any Migration: The Assessment Phase
The mistake that produces the most expensive failures is starting migration work before the assessment is complete. Teams map the documented architecture, miss the undocumented dependencies, and discover them when migration work is already in progress.
The assessment has to establish what the system actually does, not what the documentation says it does. This means:
Inventory every running service and scheduled job. Pull the complete list of Windows Services and SQL Server Agent jobs from every server in the environment. Cross-reference each against available documentation. Anything undocumented goes into a high-priority investigation queue before any migration work begins.
Map the integration surface. Review all outbound and inbound connections: API calls, FTP, file system reads and writes, email-based integrations, direct database connections from external systems. Document the business purpose of each one, not just the technical mechanism. Silent integrations that fail without alerting anyone are a separate category of risk from technical debt.
Find where the business logic actually lives. In .NET Framework monoliths, business logic migrates over time toward wherever it was easiest to put it. Check stored procedures for conditional logic rather than just data operations. Look for SQL Agent jobs that calculate values and write results to tables the application then reads. Review ASP.NET event handlers and code-behind files for domain logic rather than presentation logic. Check web.config for values that control business behavior rather than infrastructure behavior.
Categorize what can be changed safely. Code with test coverage and low coupling can be modified with reasonable confidence. Code with no test coverage that supports a critical business process should be treated as read-only until coverage is established and dependencies are mapped.
At Blackthorn Vision, this assessment phase consistently surfaces findings the internal team did not know about. In one engagement, a SQL Agent job running once a month was calculating values for a financial reconciliation process that nobody on the current team knew existed. It had been running for seven years. The migration plan had not accounted for it. Since then, Blackthorn Vision has made the full assessment phase mandatory before proposing migration timelines or delivery estimates for any legacy .NET platform.
How to Pick the First Component to Migrate
Choosing the wrong starting point is one of the most common reasons strangler migrations stall. The instinct is to start with something small and low-risk. This produces a migration that validates the toolchain without validating whether the approach works under realistic conditions.
The criteria that produce a better first component:
Clear external boundaries. The component has a defined API surface that other parts of the system consume through a stable contract, rather than reaching into shared state directly.
Measurable output. You can run both the legacy and new implementations against the same inputs and compare outputs programmatically. This is the foundation of parallel-run validation.
Meaningful traffic. The component handles enough requests that production behavior is visible in monitoring within hours, not weeks. Failure modes that only appear under real load need real load to surface.
Limited data coupling. The component does not share database tables with multiple other components in ways that make schema changes a cross-system coordination problem.
The components we typically migrate first in a .NET Framework monolith are API endpoints with well-defined request and response contracts, reporting and data export functions that can be validated by comparing output files, and background processing jobs that can be run in parallel and compared before the legacy version is disabled.
The Parallel-Run Validation Approach
Running both implementations against the same inputs and comparing their outputs is what makes the strangler fig pattern safe. Without it, you are deploying new code to production and hoping it behaves correctly.
YARP can shadow requests to both systems simultaneously. The legacy response is returned to the caller. The new service response is compared in the background. Discrepancies trigger alerts that the team investigates before increasing the traffic percentage routed to the new implementation:
// Shadow traffic middleware, sends requests to both systems,
// returns legacy response, logs discrepancies
public class ShadowTrafficMiddleware
{
private readonly RequestDelegate _next;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger _logger;
public async Task InvokeAsync(HttpContext context)
{
if (context.Request.Path.StartsWithSegments("/api/reports"))
{
// Clone request for shadow call
var shadowRequest = CloneRequest(context.Request);
// Execute both in parallel
var legacyTask = ForwardToLegacy(context.Request);
var shadowTask = ForwardToNewService(shadowRequest);
await Task.WhenAll(legacyTask, shadowTask);
var legacyResponse = await legacyTask;
var shadowResponse = await shadowTask;
if (!ResponsesMatch(legacyResponse, shadowResponse))
{
_logger.LogWarning(
"Shadow traffic discrepancy on {Path}: legacy={Legacy}, new={New}",
context.Request.Path,
legacyResponse.StatusCode,
shadowResponse.StatusCode);
}
// Always return the legacy response
await WriteResponse(context, legacyResponse);
return;
}
await _next(context);
}
}
This parallel-run approach requires observability infrastructure that many legacy .NET systems lack. If the existing system has no structured logging and no distributed tracing, that investment has to happen before the migration can proceed safely. Shadow validation has become a standard checkpoint in Blackthorn Vision modernization engagements before production traffic is shifted to any newly migrated component.
The Database Problem
The failure mode most likely to produce data corruption rather than just downtime is allowing both systems to write to the same database table simultaneously without a coordination mechanism.
When a component is being migrated, there is a period where both the legacy system and the new .NET 8 service may need to read from or write to the same data. The correct approach is to never allow concurrent writes to the same table from both systems. Options that avoid this:
Dual-write with application-level coordination. The new service writes to both the new data store and the legacy table. The legacy system reads only from its own table. This gives the new service a migration path without creating concurrent write conflicts.
Change Data Capture (CDC). Use SQL Server CDC to synchronize records between the old and new data stores without allowing both systems to write the same rows simultaneously.
Read-only shadow period. During parallel-run validation, the new service reads data but does not write. Writes remain on the legacy system until the new service is fully validated.
Database coordination consistently becomes one of the highest-risk areas identified during Blackthorn Vision assessments, and it is the failure mode most likely to produce data corruption rather than just downtime. Session state is a related problem specific to .NET Framework applications. In-process session state breaks immediately when traffic starts flowing through a YARP proxy to a different process. Externalizing session state to Azure Cache for Redis or another distributed session provider before the migration begins removes this as a blocker:
// Move session state to Redis before routing any traffic through YARP
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration["Redis:ConnectionString"];
options.InstanceName = "SessionCache:";
});
builder.Services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(30);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
Migration Sequencing for a Full Monolith
A strangler fig migration for a mid-size .NET Framework 4.x monolith typically runs over 12 to 18 months when executed alongside normal product delivery. The migration progresses in three broad phases.
Phase one (weeks 1 to 8): Infrastructure and first component. YARP is deployed. Observability through Application Insights and Azure Monitor is in place. The parallel-run validation mechanism is working. The first component has been migrated and validated under real production load. This phase is the most important: if the infrastructure is not solid, every subsequent migration step will be slower and riskier than it needs to be.
Phase two (months 2 to 15): Main migration loop. One component per sprint. Parallel-run validation. Traffic ramp. Monitoring period. Then the next component. The speed of this phase depends on the quality of the boundaries in the original system. Components with clear API surfaces migrate in days. Components where business logic is scattered across stored procedures, event handlers, and configuration files take longer because the boundary has to be established before the migration can happen.
Phase three (months 15 to 18): Decommissioning. Once all traffic has been routed to the new services, the legacy system enters a monitoring-only state for a final validation period before it is shut down. The YARP facade can be removed or retained as a load balancer.
One benefit that teams often underestimate: moving from .NET Framework to .NET 8 removes the dependency on Windows Server. The new services can run in Linux containers on Azure Kubernetes Service or Linux-based Azure development services plans. For organizations running large fleets of Windows Server VMs, the licensing costs associated with this shift can be substantially reduced.
What This Has to Do With AI
The reason AI integration appears in a migration article is that it is consistently the business driver behind legacy .NET modernization decisions being made right now. The architecture that results from a well-executed .NET 8 migration is also the architecture that makes Azure OpenAI and Semantic Kernel integration reliable in production. These are not coincidentally similar requirements. They are the same requirements.
Clean service boundaries allow Semantic Kernel to connect to existing .NET business logic through the dependency injection model already in place, without requiring the AI orchestration layer to reach into shared state or call internal methods directly. Async patterns throughout the migrated codebase handle the 5 to 30-second latency of LLM calls without the timeout failures that synchronous .NET Framework pipelines produce. Private Endpoints and Managed Identity, established as part of the Azure migration, provide the network isolation and credential-free authentication that enterprise AI features require for compliance. Application Insights observability, built alongside each migrated component, gives the engineering team visibility into what the AI pipeline is doing in production, which is the only way to diagnose failures that staging environments do not reproduce.
Teams that modernize their .NET Framework platform without planning for this produce a well-architected modern platform that still cannot support AI features cleanly, because the decisions that matter for AI readiness were not part of the migration design. The modernization roadmap and the AI readiness are one architectural program, not two separate projects.
What an Assessment-First Modernization Partner Looks Like
Most migration failures are not coding failures. They are assessment failures. The team started writing migration code before mapping what the system actually does, discovered dependencies mid-migration that required rework, and either stalled or produced a partially migrated codebase that is harder to work with than the original.
A modernization partner operating with genuine assessment-first discipline will:
Map undocumented dependencies including SQL Agent jobs, Windows Services, and downstream database connections before proposing a migration sequence
Establish clear migration boundaries for each component based on coupling, test coverage, and business criticality, not just technical convenience
Validate business logic location before migration begins, because business rules in stored procedures, event handlers, and configuration files require different extraction approaches than application-layer code
Introduce observability infrastructure before migrating any component, because parallel-run validation requires telemetry that many legacy systems do not have
Design explicit rollback paths for each migration step, so that any newly migrated component can be routed back to the legacy implementation if production behavior does not match expectations
This is the modernization assessment model Blackthorn Vision follows. The target architecture, migration sequence, and delivery timeline are all outputs of the assessment, not inputs to it.
Finding a Partner Who Has Done This
The assessment and the sequencing are where the quality of a modernization partner is most visible. McKinsey's research on technical debt consistently shows that 20 to 40 percent of enterprise technology estate value is consumed by technical debt, and that teams that defer modernization consistently find the project more expensive than teams that act earlier. Deloitte's analysis of digital operating models found that digital ownership structure is among the strongest predictors of program success. The OWASP Application Security Verification Standard provides the security framework that a .NET 8 migration on Azure should be validated against, particularly around authentication, session management, and API security.
A partner who proposes a migration plan before assessing what the system actually does has not done the work that makes the plan realistic. A partner who uses the big-bang approach on a system that has been in production for a decade is proposing the approach with the highest failure rate for exactly that type of system.
Blackthorn Vision's approach to application modernization starts with an honest assessment of what the system actually does and what it will take to migrate it without stopping product delivery. If you are evaluating options for a .NET Framework modernization, the Blackthorn Vision Clutch profile has verified client feedback on how these engagements run in practice.

Top comments (0)