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:

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.

Example.cs
// 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();
}
Example.cs
// 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.

Example.cs
// Bad — System.Web dependency
public string GetClientIp()
{
    return HttpContext.Current.Request.UserHostAddress;
}
Example.cs
// 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.

Program.cs
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.

terminal
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:

  1. Assess — Copilot analyses your project structure, dependencies, and code patterns to produce a comprehensive report of breaking changes, API compatibility issues, and deprecated patterns.
  2. Plan — Based on the assessment, it builds a migration plan with specific steps and dependency ordering.
  3. 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:

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