How to migrate from .NET Framework to .NET 10 LTS — breaking changes, EF Core 10, middleware pipeline, authentication, and phased delivery. Updated August 2026.
How to migrate from .NET Framework to .NET 10 LTS — breaking changes, EF Core 10, middleware pipeline, authentication, and phased delivery. Updated August 2026.
If your application is running on .NET Framework and you haven't started planning a migration yet, the timeline just became urgent. Both .NET 8 and .NET 9 reach end of support on November 10, 2026 after that date, Microsoft will no longer provide servicing updates, security fixes, or technical support for these versions. That gives you approximately three months from the time this article was published.
.NET 10 is an LTS release supported until November 10, 2028, and Microsoft strongly recommends that production applications upgrade to .NET 10 to take advantage of the extended support window, significant performance improvements, and new capabilities. It is the correct migration target not .NET 8, which reaches EOL in the same November 2026 window, and not .NET 9, which is STS and EOL on the same date.
This guide covers what the .NET Upgrade Assistant doesn't the strategy, the breaking change categories that require real decisions, the EF Core 10 migration path, the middleware and authentication changes, and how to structure a migration that keeps a production system running throughout.
The previous LTS release .NET 8 reaches end of support on November 10, 2026. Whether you played it safe with .NET 8 LTS or chased the latest features with .NET 9, you now face the same migration deadline. This is not a typo. Migrating from .NET Framework to .NET 8 today means immediately facing another migration to .NET 10 within months. Migrating directly to .NET 10 gives you three years of LTS support through November 2028 the longest supported window currently available in the .NET ecosystem.
.NET 10 is an LTS release recommended for enterprise and production use, with Microsoft specifically advising that production applications upgrade to benefit from its extended support window, performance gains, and new capabilities.
What's meaningfully different in .NET 10 vs .NET 8 for a migration:
Before choosing an approach, honestly assess what you're migrating. Not every .NET Framework application should be migrated to .NET 10 using the same pattern.
Strong migration candidates:
Applications that need a different approach:
ASP.NET Web Forms applications
Web Forms' page lifecycle, postback model, and server controls have no equivalent in ASP.NET Core 10. Migration is effectively a UI rewrite. The Upgrade Assistant handles project file conversion but cannot convert .aspx pages to Razor. These applications are often better migrated to Blazor or Razor Pages with the frontend rebuilt while preserving backend logic.
WCF service applications
WCF is not supported on .NET Core or .NET 10. WCF services need to be migrated to REST APIs, gRPC, or CoreWCF. See our WCF to ASP.NET Core migration guide.
Heavily COM-dependent applications
COM interop works on .NET 10 on Windows but has deployment constraints. Heavy COM dependency needs individual assessment per component.
Approach 1: In-Place Migration
Convert the existing project to target .NET 10, fix breaking changes, update packages, resolve incompatibilities within the same codebase. The application runs on .NET Framework until migration is complete, then switches on deployment.
When this works:
Approach 2: Strangler Fig Migration
A new ASP.NET Core 10 project runs alongside the existing .NET Framework application. YARP routes traffic to whichever system handles each endpoint. Components migrate incrementally.
When this works:
The key constraint: Both applications share a database during migration. Schema changes must be coordinated the .NET Framework application's EF6 and the new application's EF Core 10 cannot both apply migrations to the same database without a coordination strategy. Typical approach: freeze schema changes during the migration window, or route all schema changes through a single migration coordinator.
Run the .NET Upgrade Assistant in analysis mode first:
# Install the latest Upgrade Assistant
dotnet tool install -g upgrade-assistant
# Analyze without applying changes
upgrade-assistant analyze ./YourSolution.sln \
--target-tfm-support LTS # targets .NET 10 as LTSThe analysis report identifies:
NuGet package compatibility for .NET 10
Key packages requiring replacement:
<thscope="col">.NET Framework Package <thscope="col">.NET 10 Replacement
| EntityFramework (EF 6) | Microsoft.EntityFrameworkCore v10 |
| System.Web.Mvc | Microsoft.AspNetCore.Mvc |
| Microsoft.Owin.* | ASP.NET Core middleware |
| System.Web.Http (Web API 2) | Microsoft.AspNetCore.Mvc |
| Microsoft.AspNet.Identity | Microsoft.AspNetCore.Identity |
| Swashbuckle (old) | Microsoft.AspNetCore.OpenApi (built-in .NET 10) |
| Newtonsoft.Json | Still works or migrate to System.Text.Json |
| log4net / NLog | Still work or use Microsoft.Extensions.Logging |
System.Web dependency assessment System.Web does not exist in .NET 10. Any code referencing System.Web directly (very common in ASP.NET MVC 5 applications) needs rewriting using ASP.NET Core equivalents. The extent of this dependency is the primary driver of migration complexity.
Before (.NET Framework):
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
</PropertyGroup>
<ItemGroup>
<Reference Include="System.Web" />
</ItemGroup>
<ItemGroup>
<Compile Include="Controllers\HomeController.cs" />
<!-- every .cs file listed explicitly -->
</ItemGroup>
</Project>After (SDK-style for .NET 10):
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="10.*" />
</ItemGroup>
</Project>The SDK-style format automatically includes all .cs files no explicit <Compile> entries. The Upgrade Assistant handles this conversion, but verify the output before proceeding.
Optional: Migrate to the new .slnx solution format:
dotnet solution migrate
The new .slnx solution format replaces the 2002-era .sln format and can be migrated with one command. Not required for migration but worth doing the .slnx format is cleaner and resolves long-standing merge conflict issues with the legacy format.
EF6 and EF Core 10 share conceptual similarities but have significant API differences. The migration requires code changes throughout the data access layer.
Key Changes Requiring Decisions
No lazy loading by default. EF6 enables lazy loading automatically navigation properties load when accessed. EF Core 10 disables it by default. Code relying on automatic lazy loading throws exceptions or returns null. Your three options:
Enable lazy loading via UseLazyLoadingProxies() (quick fix, not recommended long-term)
Convert to eager loading with .Include() (recommended)
Use explicit loading where needed
Native AOT support in EF Core 10 a new .NET 10 capability. For applications targeting Native AOT compilation, EF Core 10 adds model compilation that eliminates runtime reflection. If you're targeting containerized or serverless deployments, this is worth configuring:
// In your DbContext enables Native AOT compatibility
[DbContext(typeof(AppDbContext))]
partial class AppDbContextModel : ModelBuildingConventionSet { }Database.SqlQuery<T> → FromSqlRaw or FromSqlInterpolated:
// EF6
var results = context.Database
.SqlQuery<ProductDto>("SELECT * FROM Products WHERE Id = @p0", id)
.ToList();
// EF Core 10
var results = context.Products
.FromSqlInterpolated($"SELECT * FROM Products WHERE Id = {id}")
.ToList();
// Or using ExecuteSqlAsync for non-query operations
await context.Database.ExecuteSqlAsync(
$"UPDATE Products SET Stock = {newStock} WHERE Id = {id}");Register DbContext correctly in .NET 10:
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(
builder.Configuration.GetConnectionString("Default")));
// For high-throughput scenarios DbContext pooling
builder.Services.AddDbContextPool<AppDbContext>(options =>
options.UseSqlServer(connectionString),
poolSize: 128);EF Core 10 migrations from an existing database:
Scaffold models from existing database: dotnet ef dbcontext scaffold "..." Microsoft.EntityFrameworkCore.SqlServer
Create an initial migration representing current state: dotnet ef migrations add InitialCreate
Mark as already applied: dotnet ef database update 0 then insert the migration record manually
From this point, use EF Core migrations normally
Global.asax and web.config for application configuration are replaced entirely by Program.cs:
var builder = WebApplication.CreateBuilder(args);
// Service registration
builder.Services.AddControllersWithViews();
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(
builder.Configuration.GetConnectionString("Default")));
// .NET 10: Built-in OpenAPI (replaces Swashbuckle)
builder.Services.AddOpenApi();
// Register all application services
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddHttpContextAccessor();
var app = builder.Build();
// Middleware pipeline order matters
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
// .NET 10: OpenAPI endpoint
app.MapOpenApi();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();Replacing Application_Error:
// Global exception handling middleware replaces Application_Error
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
var feature = context.Features
.Get<IExceptionHandlerFeature>();
var exception = feature?.Error;
_logger.LogError(exception, "Unhandled exception");
context.Response.StatusCode = 500;
await context.Response.WriteAsJsonAsync(new ProblemDetails
{
Title = "An unexpected error occurred",
Status = 500
});
});
});Forms Authentication → ASP.NET Core Cookie Authentication:
builder.Services.AddAuthentication(
CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Account/Login";
options.AccessDeniedPath = "/Account/AccessDenied";
options.ExpireTimeSpan = TimeSpan.FromMinutes(60);
options.SlidingExpiration = true;
});ASP.NET Identity 2.x → ASP.NET Core Identity 10:
ASP.NET Core Identity 10 is the current version ships with .NET 10. The user data migrates but the password hashing algorithm changed. Handle legacy passwords with a custom hasher:
// Custom password hasher supporting both legacy and new hash formats
public class LegacyPasswordHasher : PasswordHasher<ApplicationUser>
{
public override PasswordVerificationResult VerifyHashedPassword(
ApplicationUser user,
string hashedPassword,
string providedPassword)
{
// Try the new algorithm first
var result = base.VerifyHashedPassword(
user, hashedPassword, providedPassword);
if (result == PasswordVerificationResult.Failed)
{
// Fall back to legacy hash verification
if (VerifyLegacyHash(hashedPassword, providedPassword))
return PasswordVerificationResult.SuccessRehashNeeded;
}
return result;
}
}Register the custom hasher:
builder.Services.AddScoped<IPasswordHasher<ApplicationUser>,
LegacyPasswordHasher>();SuccessRehashNeeded tells ASP.NET Core Identity to update the stored hash to the new algorithm on the user's next successful login transparently migrating the password store without requiring a password reset.
web.config AppSettings → appsettings.json:
{
"ConnectionStrings": {
"Default": "Server=...;Database=...;Trusted_Connection=true;"
},
"ApiSettings": {
"BaseUrl": "https://api.example.com",
"TimeoutSeconds": 30
},
"FeatureFlags": {
"NewCheckout": true,
"BetaDashboard": false
}
}Strongly-typed configuration (recommended over IConfiguration["key"]):
// Option class
public class ApiSettings
{
public string BaseUrl { get; set; } = default!;
public int TimeoutSeconds { get; set; } = 30;
}
// Register in Program.cs
builder.Services.Configure<ApiSettings>(
builder.Configuration.GetSection("ApiSettings"));
// Inject into services
public class ApiClient(IOptions<ApiSettings> settings)
{
private readonly ApiSettings _settings = settings.Value;
}Secrets in production: Connection strings and API keys must move to Azure Key Vault or environment variables. This is a .NET 10 best practice enforced by the configuration system:
// Azure Key Vault integration no credentials required with Managed Identity
builder.Configuration.AddAzureKeyVault(
new Uri("https://your-vault.vault.azure.net/"),
new DefaultAzureCredential());.NET Framework MVC used third-party containers (Unity, Autofac, Ninject). ASP.NET Core 10 has first-class built-in DI:
// Lifetime registrations in Program.cs builder.Services.AddSingleton<ICacheService, RedisCacheService>(); builder.Services.AddScoped<IProductRepository, ProductRepository>(); builder.Services.AddScoped<IOrderService, OrderService>(); builder.Services.AddTransient<IEmailService, SendGridEmailService>();
If your DI configuration is complex and uses Autofac-specific features (decorators, scanning, named registrations), Autofac has a .NET 10 integration package that lets you keep your existing container configuration while using ASP.NET Core's hosting model. This reduces migration scope significantly for applications with extensive DI configuration.
HttpContext.Current → IHttpContextAccessor:
// Register in Program.cs
builder.Services.AddHttpContextAccessor();
// Inject where needed not everywhere
public class UserContextService(IHttpContextAccessor accessor)
{
public string? GetCurrentUserId()
=> accessor.HttpContext?
.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
}Use IHttpContextAccessor only where genuinely necessary prefer passing user identity as method parameters where possible.
Session in ASP.NET Core 10:
// Registration
builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(30);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
app.UseSession(); // before UseRouting
// Usage
HttpContext.Session.SetString("CartId", cartId.ToString());
var cartId = HttpContext.Session.GetString("CartId");For multi-server deployments on Azure App Service, replace AddDistributedMemoryCache with Redis:
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration =
builder.Configuration["Redis:ConnectionString"];
});BundleConfig.cs and System.Web.Optimization are replaced by ASP.NET Core 10's static file middleware:
app.UseStaticFiles(); // serves files from wwwroot/
Move files: ~/Content/ → wwwroot/css/, ~/Scripts/ → wwwroot/js/, ~/Images/ → wwwroot/images/.
For bundling and minification in .NET 10, two options:
WebOptimizer (server-side, minimal config):
builder.Services.AddWebOptimizer(pipeline =>
{
pipeline.AddCssBundle("/css/bundle.css",
"css/site.css", "css/theme.css");
pipeline.AddJavaScriptBundle("/js/bundle.js",
"js/app.js", "js/utils.js");
});Build-time tools (Vite, Webpack, esbuild) preferred for applications with React, Angular, or Vue frontends.
Testing Strategy
Establish HTTP-level integration tests before migration begins they test behavior rather than implementation, so they survive the migration:
public class ProductControllerTests
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public ProductControllerTests(WebApplicationFactory<Program> factory)
{
_client = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
services.RemoveAll<AppDbContext>();
services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase("TestDb"));
});
}).CreateClient();
}
[Fact]
public async Task GetProducts_ReturnsOk()
{
var response = await _client.GetAsync("/api/products");
response.EnsureSuccessStatusCode();
}
}Why migrate to .NET 10 instead of .NET 8?
Both .NET 8 and .NET 9 will reach end of support on November 10, 2026. Migrating to .NET 8 today means immediately planning another migration before the end of this year. .NET 10 is the current LTS release with support through November 2028 migrating directly to .NET 10 gives you three years of supported life without an intermediate migration step.
How long does a .NET Framework to .NET 10 migration take?
For a clean ASP.NET MVC 5 application with a service layer: a small application (under 30K LOC) typically takes 3–6 weeks. A medium application (30K–100K LOC) typically takes 6–16 weeks. A large enterprise application using the Strangler Fig pattern typically takes 4–12 months. The biggest time variables are extent of System.Web usage, EF6 lazy loading dependency, and available test coverage.
Can we migrate one project at a time within a large solution?
Yes SDK-style project files support multi-targeting, allowing a class library to target both .NET Framework 4.8 and net10.0 simultaneously during the transition:
<TargetFrameworks>net48;net10.0</TargetFrameworks>
This allows the web project to migrate while shared libraries support both old and new web projects.
What happens to EF6 migrations when migrating to EF Core 10?
Existing EF6 migrations cannot be used with EF Core 10 EF Core has its own migration system and history table. The typical approach: scaffold models from the existing database, create an initial EF Core migration representing the current schema, mark it as already applied, then use EF Core migrations normally from that point forward.
Should we enable Native AOT in .NET 10?
For most migrating ASP.NET Core applications, no not immediately. Native AOT compilation in EF Core 10 is a significant capability but adds constraints (no runtime reflection, no dynamic code generation) that require code changes beyond the standard migration. Consider it as a separate optimization phase after the migration is complete and stable, specifically for containerized or serverless deployment scenarios where startup time is critical.
Is our .NET Framework application at security risk if we don't migrate before November 2026?
Yes. After November 10, 2026, Microsoft will no longer provide servicing updates, security fixes, or technical support for .NET 8 and .NET 9 and by extension the framework versions below them. Running an unpatched web framework exposes the application to known vulnerabilities without remediation. For applications handling sensitive data financial, healthcare, personal this is a compliance risk as well as a security risk.
The November 2026 EOL deadline for .NET 8 and .NET 9 makes .NET Framework to .NET 10 migration a near-term business requirement rather than a medium-term aspiration. The migration is well-understood engineering work the tooling is mature, the breaking changes are documented, and the end result (current security patches, significantly better performance, cross-platform deployment, AI-ready infrastructure with Microsoft.Extensions.AI and Microsoft Agent Framework) is consistently worth the investment.
If you're planning a .NET Framework to .NET 10 migration and want an experienced team to assess scope and lead the work, our software modernization team has delivered this migration across a range of codebase sizes from focused 4-week migrations to multi-phase enterprise projects running alongside normal development.