How I Upgraded a Legacy .NET Core 3.1 Project to .NET 8—With Minimal Pain


Upgrading a legacy application sounds exciting until you realize how many things can break. I recently led the migration of a production .NET Core 3.1 application to .NET 8, and it turned out to be smoother than expected, thanks to a disciplined approach and some hard-earned lessons.
In this article, I’ll walk you through:
Why we upgraded
The challenges we faced
A step-by-step upgrade strategy
Best practices and tooling
Real code examples
✨ Why Upgrade from .NET Core 3.1 to .NET 8?
.NET 8 brings several compelling improvements:
Long-Term Support (LTS)
Better performance and memory management
Minimal APIs and cleaner hosting model
Unified platform across workloads
.NET Core 3.1 reached end of support, so staying on it posed a security and maintenance risk.
⚠️ Key Challenges We Encountered
Third-party NuGet packages were outdated or deprecated
Incompatible ASP.NET Core middleware or APIs
Breaking changes in EF Core and project SDKs
Custom MSBuild tasks failing due to SDK changes
CI/CD pipeline adjustments for new SDKs and runtime
📆 Step-by-Step Upgrade Process
1. Inventory All Projects and Dependencies
List all class libraries, web apps, test projects
Check their dependencies (.NET version, NuGet packages)
Use tools like
dotnet list reference
ordotnet-project-graph
to understand how projects are linkedConsider using dependency visualization tools (e.g., NDepend or Project Dependency Viewer in Visual Studio) to identify tight coupling or unnecessary references
List all class libraries, web apps, test projects
Check their dependencies (.NET version, NuGet packages)
2. Update SDKs and Target Frameworks
<TargetFramework>net8.0</TargetFramework>
Also update
global.json
if usedUse
dotnet --list-sdks
to verify the .NET 8 SDK is installed on your systemBe aware that
global.json
may force your build to use an older SDK; ensure it aligns with the version you're upgrading to
<TargetFramework>net8.0</TargetFramework>
- Also update
global.json
if used
3. Refactor Startup and Program Files
From:
public class Startup
{
public void ConfigureServices(IServiceCollection services) { }
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { }
}
To:
var builder = WebApplication.CreateBuilder(args);
// Register services
builder.Services.AddControllers();
var app = builder.Build();
// Configure middleware
app.UseRouting();
app.UseAuthorization();
app.MapControllers();
app.Run();
This shift from Startup.cs
to the minimal hosting model is a major architectural change in .NET 6 and later. You need to:
Move
ConfigureServices
logic intobuilder.Services
Transfer all
Configure
middleware logic (e.g.,UseRouting
,UseAuthentication
,UseAuthorization
, etc.) into the new middleware pipeline beforeapp.Run()
Ensure custom middlewares and service registrations (like Swagger, CORS, logging) are manually migrated From:
public class Startup
{
public void ConfigureServices(IServiceCollection services) { }
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { }
}
To:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapControllers();
app.Run();
4. Replace or Upgrade Deprecated Packages
Use
dotnet list package --outdated
Replace unsupported libraries (e.g., logging, auth, UI components)
5. Handle Breaking Changes in EF Core
Re-run migrations using the latest EF Core CLI commands
Carefully review the official EF Core 8 breaking changes documentation
Pay attention to changes in behavior for methods like
AsNoTracking()
,Include()
, and transaction handlingEF Core 8 has performance improvements that may require tuning or testing your LINQ queries
Consider using EF Core Power Tools to reverse engineer or visually analyze your DbContext and model mappings
Review your database providers and ensure they are fully compatible with EF Core 8
Re-run migrations
Check for
AsNoTracking()
and other method behavior changes
6. Update Unit Tests and Mocks
Update test projects to target .NET 8
Replace deprecated test libraries
7. Revise CI/CD Pipeline and Docker Files
- Use
.NET 8
SDK base images
FROM mcr.microsoft.com/dotnet/aspnet:8.0
- Update build agents and runtime checks in your CI pipeline
For Azure DevOps:
Update
azure-pipelines.yml
to usewindows-latest
orubuntu-latest
agents with .NET 8 installedAdd a
UseDotNet@2
task to install .NET 8 SDK if needed
For GitHub Actions:
- Ensure
actions/setup-dotnet@v3
specifiesdotnet-version: '8.x'
- uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.x'
For GitLab CI:
Update
image:
to use a .NET 8 compatible Docker imageAdd
.NET restore
,build
, andtest
steps with updated SDK paths
Ensure Dockerfiles and CI runners match your upgraded framework version to prevent runtime conflicts.
- Use
.NET 8
SDK base images
FROM mcr.microsoft.com/dotnet/aspnet:8.0
- Update build agents and runtime checks
8. Smoke Test Everything
Local testing
CI automation
Staging environment validation
🔧 Tools That Helped
Upgrade Assistant:
dotnet tool install -g upgrade-assistant
.NET Portability Analyzer
SonarQube / Resharper for code quality and cleanup
NuKeeper / Renovate for dependency versioning
💼 Best Practices
Upgrade library projects before applications
Use separate branches to isolate upgrade work
Freeze feature development during the upgrade
Communicate the changes to QA/DevOps early
Monitor production logs post-deployment
🔍 Final Thoughts
The move from .NET Core 3.1 to .NET 8 is more than a version bump, it’s an opportunity to modernize and clean up your codebase. If done thoughtfully, you can future-proof your application without introducing chaos.
If you’re planning your upgrade soon: start small, test often, and document everything.
Happy upgrading!
Subscribe to my newsletter
Read articles from Vaibhav directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Vaibhav
Vaibhav
I break down complex software concepts into actionable blog posts. Writing about cloud, code, architecture, and everything in between.