Upgrading from .NET 6 to .NET 8: A Practical Checklist
.NET 6 reached end-of-support in November 2024. If you're still on it, you're running without security patches. .NET 8 is the current Long-Term Support release, supported until November 2026. Here's how to make the jump.
Before you start
Check your dependencies. Run dotnet list package --outdated on every project in your solution. Third-party packages that target net6.0 specifically (rather than netstandard2.0 or netstandard2.1) may need updating. Pay particular attention to:
- Entity Framework Core (6.x to 8.x)
- ASP.NET Core identity packages
- Third-party authentication libraries
- Hosting/deployment packages (Serilog, health check libraries)
Install the .NET 8 SDK. Download it from the official site and verify with dotnet --list-sdks. Consider using a global.json to pin the SDK version for your team.
Step 1: Update the Target Framework
In every .csproj file, change the TFM:
<!-- Before -->
<TargetFramework>net6.0</TargetFramework>
<!-- After -->
<TargetFramework>net8.0</TargetFramework>
If you have a Directory.Build.props that sets the TFM centrally, update it there instead.
For libraries that need to support multiple runtimes, use multi-targeting:
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
Step 2: Update NuGet packages
Update all Microsoft packages to their 8.x versions. If you're using Central Package Management:
<!-- Directory.Packages.props -->
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
Without central management, update each project individually:
dotnet add package Microsoft.EntityFrameworkCore --version 8.0.*
Step 3: Handle breaking changes
Validated options by default
.NET 8's AddOptions<T>() with ValidateOnStart() now throws during startup if validation fails, rather than on first access. This is generally desirable, but it may surface latent configuration errors that were previously hidden.
JSON serialization defaults
System.Text.Json in .NET 8 includes JsonStringEnumConverter<TEnum> (a generic, source-gen-friendly version). If you're using the non-generic JsonStringEnumConverter, it still works but consider migrating.
EF Core 8 changes
If you're upgrading Entity Framework Core alongside the runtime:
// EF Core 8 requires DateOnly/TimeOnly mapping updates
// The default value converter behaviour has changed
// Before (EF Core 6)
modelBuilder.Entity<Event>()
.Property(e => e.Date)
.HasConversion<string>();
// After (EF Core 8) - DateOnly is natively supported
// No converter needed for SQL Server and PostgreSQL
Complex type support was added in EF Core 8. If you were using owned types as a workaround, consider migrating to [ComplexType].
HTTP logging changes
UseHttpLogging middleware now redacts request headers by default. If you rely on logged headers for debugging, explicitly configure which headers to include:
builder.Services.AddHttpLogging(options =>
{
options.RequestHeaders.Add("X-Correlation-Id");
options.ResponseHeaders.Add("X-Correlation-Id");
});
Step 4: Update Docker images
If you deploy via Docker, update your base images:
# Before
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS runtime
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
# After
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
.NET 8 images default to a non-root user. If your application writes to the filesystem or binds to port 80, you may need to adjust:
# If you need port 80 (default is now 8080)
ENV ASPNETCORE_HTTP_PORTS=80
Step 5: Adopt new features incrementally
You don't have to use every .NET 8 feature on day one, but some are worth adopting early:
Frozen collections for static lookup data:
private static readonly FrozenDictionary<string, int> StatusCodes =
new Dictionary<string, int>
{
["OK"] = 200,
["NotFound"] = 404,
["ServerError"] = 500
}.ToFrozenDictionary();
Keyed services in DI:
builder.Services.AddKeyedSingleton<ICache>("redis", new RedisCache());
builder.Services.AddKeyedSingleton<ICache>("memory", new MemoryCache());
// Inject with [FromKeyedServices("redis")] ICache cache
Time abstraction for testability:
builder.Services.AddSingleton(TimeProvider.System);
// In your service
public class TokenService(TimeProvider timeProvider)
{
public bool IsExpired(Token token)
=> timeProvider.GetUtcNow() > token.ExpiresAt;
}
Step 6: Update CI/CD
Update your CI pipeline to use the .NET 8 SDK. For GitHub Actions:
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
Validation
After upgrading, run your full test suite. Pay attention to:
- Serialization/deserialization tests (JSON behaviour changes)
- Integration tests involving EF Core (query translation changes)
- Authentication and authorisation flows
- Health check endpoints
- Any code that inspects
RuntimeInformationorEnvironment.Version
Wrapping up
The .NET 6 to .NET 8 upgrade is generally smooth for well-tested applications. The most common friction points are EF Core changes, Docker port defaults, and third-party package compatibility. Tackle it project by project, starting with the ones that have the best test coverage, and work outward from there.