If you have ever looked at a version number and felt confused, ASP.NET Core 2.3 is the gift that keeps on giving. On April 7, Microsoft announced that ASP.NET Core 2.3 on .NET Framework will reach end of support exactly one year later, on April 7, 2027. After that date, no security patches, no bug fixes, and no technical support.
For teams that have been running ASP.NET Core on .NET Framework as a comfortable middle ground between legacy and modern, the clock is now ticking. You have twelve months to move off the last supported bridge between ASP.NET Core and .NET Framework — and if you have not started planning, this is your wake-up call.
This is not a surprise if you have been watching the trajectory. Microsoft dropped .NET Framework support from ASP.NET Core 3.0 back in 2019. The 2.x line on .NET Framework has been living on borrowed time ever since. But understanding what 2.3 actually is, why it exists, and what migration looks like in practice requires digging into one of .NET's stranger versioning stories.
The version number that makes no sense (until it does)
ASP.NET Core 2.3 is not what it looks like. It is not a release that came after 2.2 with new features and improvements. It is ASP.NET Core 2.1, reshipped under a higher version number. Same code, same APIs, same behaviour. The version number changed; the bits did not.
Here is why. ASP.NET Core 2.1 was the last Long Term Support release that ran on .NET Framework. Version 2.2 shipped with breaking changes — a violation of semantic versioning expectations for a minor bump — and then went out of support in December 2019. That left developers in an awkward spot: if you had upgraded to 2.2, you were on an unsupported version, and rolling back to 2.1 was not a simple dotnet restore away.
ASP.NET Core on .NET Framework ships as a set of NuGet packages — well over one hundred of them. NuGet's dependency resolution is designed around upgrades, not downgrades. If your project referenced 2.2 packages, downgrading to 2.1 meant NuGet would refuse to restore dependencies that had themselves upgraded to target 2.2. The practical reality was that going back to a supported version required manually untangling a dependency graph that nobody wanted to touch.
Microsoft's solution was pragmatic if unconventional: take the 2.1 codebase, stamp it as 2.3, and ship it. Users on 2.2 could now move to a supported version through what NuGet treats as a normal upgrade. Users on 2.1 would be pulled forward automatically, with no breaking changes since the underlying code was identical.
// NOTE
ASP.NET Core 2.3 applies only to ASP.NET Core running on .NET Framework. If you are running ASP.NET Core on .NET Core, the 2.x line has been unsupported for years. The migration target is .NET 10.
What end of support actually means
Let us be clear about what happens on April 8, 2027: nothing dramatic. Your applications will not stop working. IIS will not refuse to host them. The runtime does not self-destruct.
What changes is the safety net. After the end-of-support date:
- No security patches. Any vulnerability discovered in the 2.3 packages will not be fixed. Your application will carry unpatched CVEs indefinitely.
- No bug fixes. Known issues will remain known and unfixed.
- No technical support. Microsoft Support will not assist with ASP.NET Core 2.3 issues.
- Packages marked deprecated. NuGet will flag your dependencies, and build warnings will appear.
Microsoft classifies ASP.NET Core 2.3 as a "Tool" under their Support Lifecycle Policy, which requires only 12 months' advance notice before ending support. This is a shorter runway than many enterprise teams are used to, but it is the policy that applies here.
For organisations subject to compliance requirements — PCI DSS, SOC 2, HIPAA, or similar — running unsupported frameworks is typically a finding that auditors will flag. The operational risk is real even if the technical risk seems manageable.
The migration is not a version bump
If you are on ASP.NET Core 2.3 running on .NET Framework, your migration is not a simple target framework change. You are not going from net48 to net10.0 and calling it a day. You are crossing a fundamental platform boundary.
ASP.NET Core on .NET Framework relies on System.Web, the IIS hosting model, and .NET Framework's runtime. Modern ASP.NET Core on .NET 10 uses Kestrel, the middleware pipeline, and a completely different hosting model. The gap between these two worlds is substantial.
Here is what you are likely dealing with:
Dependencies on .NET Framework-only libraries
The biggest blocker for most migrations is third-party or internal libraries that target .NET Framework exclusively. These might be COM interop wrappers, libraries using System.Drawing without the cross-platform shims, or internal packages that nobody has touched in years.
// Bad — .NET Framework-only dependency
using System.Drawing;
public byte[] GenerateThumbnail(Stream input)
{
using var image = Image.FromStream(input);
using var thumbnail = image.GetThumbnailImage(100, 100, null, IntPtr.Zero);
using var ms = new MemoryStream();
thumbnail.Save(ms, ImageFormat.Png);
return ms.ToArray();
}
// Good — cross-platform alternative using SkiaSharp
using SkiaSharp;
public byte[] GenerateThumbnail(Stream input)
{
using var original = SKBitmap.Decode(input);
using var resized = original.Resize(new SKImageInfo(100, 100), SKFilterQuality.Medium);
using var image = SKImage.FromBitmap(resized);
using var data = image.Encode(SKEncodedImageFormat.Png, 90);
return data.ToArray();
}
System.Web dependencies
If your code touches HttpContext.Current, HttpRuntime, or anything from System.Web, those APIs do not exist in modern ASP.NET Core. The middleware pipeline and dependency injection model are fundamentally different.
// Bad — System.Web dependency
public string GetClientIp()
{
return HttpContext.Current.Request.UserHostAddress;
}
// Good — ASP.NET Core equivalent via dependency injection
public class ClientInfoService(IHttpContextAccessor httpContextAccessor)
{
public string? GetClientIp()
{
return httpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString();
}
}
WCF services
If you are hosting or consuming WCF services, there is no direct equivalent in modern .NET. Microsoft's recommended path is gRPC for service-to-service communication. The CoreWCF project provides a bridge for hosting WCF-style services on modern .NET, but it is not a drop-in replacement.
Global.asax and the startup model
The Global.asax lifecycle does not exist in modern ASP.NET Core. Application startup, middleware registration, and service configuration happen through the WebApplicationBuilder pattern.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddAuthentication()
.AddJwtBearer();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Planning the migration
A twelve-month runway sounds generous until you account for the reality of enterprise software development. Here is a practical approach.
Step 1: Assess your dependency graph
Before writing any migration code, understand what you are working with. The .NET Portability Analyser and the .NET Upgrade Assistant can scan your solution and identify APIs that are not available on modern .NET.
dotnet tool install -g upgrade-assistant
upgrade-assistant analyze ./MySolution.sln
The Upgrade Assistant generates a report listing every incompatible API, missing packages, and framework-specific dependencies. This is your starting point for estimating the migration effort.
Step 2: Identify your migration strategy
For most teams, the choice comes down to two approaches:
Incremental migration works when you can extract parts of the application into separate services running on modern .NET while keeping the legacy application running. This is lower risk but takes longer.
Big-bang migration converts everything at once. This is faster if the application is small enough, but risky for large codebases with extensive integration test suites.
// TIP
If your application has fewer than 50,000 lines of code and reasonable test coverage, a big-bang migration is usually feasible. Above that threshold, consider an incremental approach.
Step 3: Use the tooling
GitHub Copilot modernisation is now generally available as a VS Code extension. It follows an Assess, Plan, Execute model:
- Assess — Copilot analyses your project structure, dependencies, and code patterns to produce a comprehensive report of breaking changes, API compatibility issues, and deprecated patterns.
- Plan — Based on the assessment, it builds a migration plan with specific steps and dependency ordering.
- Execute — Copilot can generate the actual code changes, updated project files, and containerisation artefacts.
This is not going to handle every edge case, but it significantly accelerates the mechanical parts of the migration — updating project files, replacing API calls, and restructuring startup code.
// WARNING
AI-assisted migration tools are excellent at the mechanical transformation but will not catch subtle behavioural differences. Always verify with integration tests, particularly around authentication, authorisation, and data access.
Step 4: Address the hosting model
Your .NET Framework application almost certainly runs on IIS. Modern ASP.NET Core uses Kestrel as its web server, though it can sit behind IIS as a reverse proxy. Decide early whether you are moving to:
- IIS with the ASP.NET Core Module — minimal infrastructure change, IIS acts as a reverse proxy to Kestrel
- Kestrel standalone — simpler deployment model, works well with containers
- Containers on Azure Container Apps or Kubernetes — the most modern option, with Aspire orchestration if you want it
Common pitfalls
Assuming the migration is just a recompile. It is not. The hosting model, middleware pipeline, and dependency injection system are all different. Budget time for rearchitecting, not just recompiling.
Ignoring the System.Web surface area. Audit every reference to System.Web in your codebase. This includes transitive dependencies — your code might not reference it directly, but a library you depend on might.
Skipping the authentication migration. ASP.NET Core's authentication middleware behaves differently from Forms Authentication or Windows Authentication on .NET Framework. Test authentication flows thoroughly.
Not updating Entity Framework. If you are on Entity Framework 6, you need to migrate to Entity Framework Core. The APIs are similar but not identical, and migration strategies for your database schema need careful planning.
Waiting until month eleven. Twelve months is not as long as it sounds when you factor in development sprints, QA cycles, staging deployments, and change management processes. Start the assessment now.
Forgetting about NuGet package compatibility. Some NuGet packages you rely on may not have .NET 10 compatible versions. Identify these early — you may need to find replacements or contribute updates.
Summary
- ASP.NET Core 2.3 on .NET Framework reaches end of support on April 7, 2027. After that date, no security patches or technical support will be provided.
- Version 2.3 is a re-release of 2.1 under a higher version number, created to help developers on the unsupported 2.2 upgrade to a supported version via normal NuGet package resolution.
- This is the last supported version of ASP.NET Core that runs on .NET Framework. There will be no 2.4.
- Migration to .NET 10 is not a trivial upgrade — it involves crossing a platform boundary from .NET Framework to modern .NET, with changes to hosting, middleware, dependency injection, and potentially data access.
- Use the .NET Upgrade Assistant and GitHub Copilot modernisation tooling to assess and accelerate the migration.
- Start now. Twelve months with enterprise change management is tighter than it appears.