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:

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:

config.xml
<!-- 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:

config.xml
<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:

config.xml
<!-- 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:

Example.cs
// 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:

Example.cs
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:

Dockerfile
# 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:

Dockerfile
# 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:

Example.cs
private static readonly FrozenDictionary<string, int> StatusCodes =
    new Dictionary<string, int>
    {
        ["OK"] = 200,
        ["NotFound"] = 404,
        ["ServerError"] = 500
    }.ToFrozenDictionary();

Keyed services in DI:

Example.cs
builder.Services.AddKeyedSingleton<ICache>("redis", new RedisCache());
builder.Services.AddKeyedSingleton<ICache>("memory", new MemoryCache());

// Inject with [FromKeyedServices("redis")] ICache cache

Time abstraction for testability:

Example.cs
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:

config.yaml
- uses: actions/setup-dotnet@v4
  with:
    dotnet-version: '8.0.x'

Validation

After upgrading, run your full test suite. Pay attention to:

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.