If your production applications are still running on .NET 8 or .NET 9, there is a date worth putting on your engineering calendar:

November 10, 2026.

Both .NET 8 and .NET 9 reach end of support on that date.

For many applications, changing:

<TargetFramework>net8.0</TargetFramework>

to:

<TargetFramework>net10.0</TargetFramework>

takes less than a minute.

But that is not the migration.

The real migration includes your SDK, NuGet dependencies, Entity Framework Core queries, CI environment, container images, hosting runtime, integration tests, deployment strategy, and—most importantly—the version actually running in production.

This guide focuses on that part.

The goal is not simply to make a .NET 10 application compile. The goal is to migrate a production application to .NET 10 without discovering the important problems after deployment.


Why Upgrade to .NET 10 Now?

.NET 10 is the current Long Term Support (LTS) release.

Meanwhile, both .NET 8 and .NET 9 reach end of support on November 10, 2026.

That makes .NET 10 the natural target for applications that need to remain supported beyond that date.

Waiting until the final weeks creates unnecessary risk.

A production migration may uncover:

  • incompatible NuGet packages
  • SDK differences between developer machines and CI
  • framework breaking changes
  • EF Core query behavior changes
  • container image problems
  • hosting environments with the wrong runtime
  • performance regressions
  • tests that were never exercising production-like infrastructure

Start while rollback is still an engineering decision—not an emergency.


1. Establish Your Current Baseline

Before changing anything, record what you are actually running.

From the repository, run:

dotnet --version
dotnet --info
dotnet --list-sdks
dotnet --list-runtimes

Then inspect your project files.

For .NET 8:

<PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
</PropertyGroup>

Or for .NET 9:

<PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
</PropertyGroup>

For larger repositories, check every project.

It is common to find test projects, migration utilities, workers, or internal tools targeting a different framework from the main application.

Also record:

  • current SDK version
  • target frameworks
  • ASP.NET Core version
  • EF Core version
  • database provider
  • container base images
  • CI SDK version
  • production runtime version
  • critical NuGet packages

This gives you a known baseline to compare against after the migration.

Do not start by upgrading everything simultaneously.


2. Install .NET 10, but Control Which SDK Builds the Repository

Installing the .NET 10 SDK is only the beginning.

Check the installed SDKs:

dotnet --list-sdks

A developer machine can contain several SDK versions side by side.

For example:

8.0.xxx
9.0.xxx
10.0.xxx

For production repositories, consider controlling SDK selection with global.json.

A valid example is:

{
  "sdk": {
    "version": "10.0.100",
    "rollForward": "latestFeature"
  }
}

The version property must contain a complete SDK version. Do not use a wildcard such as 10.0.x or 10.0.xxx in global.json.

Choose the SDK version and rollForward policy deliberately based on how tightly your team wants to control SDK updates.

Then verify what is actually selected:

dotnet --version

The important point is determinism.

Your laptop, CI pipeline, and release process should not accidentally test the repository with unrelated SDK versions.

Commit global.json with the repository when you use it.


3. Change the Target Framework

Now update the application:

<PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
</PropertyGroup>

For a multi-targeted library, migration might instead look like:

<PropertyGroup>
    <TargetFrameworks>net8.0;net10.0</TargetFrameworks>
</PropertyGroup>

Multi-targeting can be useful when a shared library must continue serving applications that have not migrated yet.

Immediately restore and build:

dotnet restore
dotnet build

Do not suppress warnings simply to obtain a green build.

A framework upgrade is exactly when warnings deserve attention.

After an explicit restore, you can separate the compilation step:

dotnet build --no-restore

This also makes restore and compilation failures easier to diagnose independently in CI.


4. Audit NuGet Packages Before Updating Them

A common migration mistake is:

Upgrade .NET, then upgrade every package to the newest version.

That creates too many variables at once.

First, discover what is outdated:

dotnet list package --outdated

Then inspect the dependency graph, including transitive packages:

dotnet list package --include-transitive

Pay particular attention to framework-adjacent dependencies such as:

  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Design
  • Microsoft.Extensions.*
  • authentication libraries
  • OpenAPI libraries
  • logging providers
  • database drivers
  • test infrastructure

Keep related packages aligned.

For example, do not casually create a dependency graph where EF Core runtime, provider, and tooling packages use incompatible major versions.

A safer migration sequence is:

1. Record the existing package graph
2. Retarget the framework
3. Restore
4. Resolve known incompatibilities
5. Upgrade framework-coupled packages deliberately
6. Build
7. Run tests
8. Review unrelated package upgrades separately

This makes failures much easier to attribute.


5. Treat EF Core 10 as Its Own Migration

If your application uses Entity Framework Core, do not treat the EF Core upgrade as simply changing a package number.

Major EF Core versions can introduce behavioral changes even when your application still compiles.

After upgrading, inspect your migrations:

dotnet ef migrations list

Where it fits your workflow, you can also check for model changes:

dotnet ef migrations has-pending-model-changes

More importantly, test your real queries.

A passing CRUD integration test does not tell you whether an important production query now generates different SQL.

Consider:

var customerIds = request.CustomerIds;

var orders = await db.Orders
    .Where(x => customerIds.Contains(x.CustomerId))
    .ToListAsync();

Translation of parameterized collections has changed across recent EF Core versions, and the translation strategy can affect database query plans.

For important queries, inspect the generated SQL:

var query = db.Orders
    .Where(x => customerIds.Contains(x.CustomerId));

var sql = query.ToQueryString();

Do not automatically apply a global workaround because one query becomes slower.

Different translation strategies have different trade-offs. A strategy that improves one workload can make another worse.

Measure the problematic query first.

For critical database operations, your migration test should answer more than one question:

Is the application result correct?
        ↓
Is the generated SQL reasonable?
        ↓
Is the execution plan reasonable?
        ↓
Is latency comparable?
        ↓
Is database load acceptable?

That tells you much more than simply asking whether dotnet test passed.


6. Review the Breaking Changes That Apply to Your Application

Do not read every .NET 10 change and turn the migration into a research project.

Instead, classify your application.

For example:

ASP.NET Core API
EF Core
SQL Server
JWT authentication
BackgroundService
Docker/Linux
xUnit integration tests

Then review the .NET 10 breaking changes relevant to those areas.

Pay particular attention to behavioral changes.

They are easy to miss because the application may compile perfectly.

That is why:

Build succeeded

is not equivalent to:

Migration succeeded

Compilation proves only one part of the migration.


7. Test the Application Outside Visual Studio

A migration should survive the same commands your CI environment uses.

Run:

dotnet restore
dotnet build --configuration Release
dotnet test --configuration Release
dotnet publish --configuration Release

Then test the published artifact.

For a framework-dependent application:

dotnet ./bin/Release/net10.0/publish/MyApp.dll

This catches an important class of problems:

The source project works, but the deployable artifact does not.

Your production system runs the published artifact—not your Visual Studio debugging session.


8. Upgrade the CI Pipeline Explicitly

Your local migration can succeed while CI still uses an older SDK.

For GitHub Actions, make the SDK requirement explicit:

- name: Setup .NET
  uses: actions/setup-dotnet@v4
  with:
    dotnet-version: '10.0.x'

Then make the environment visible in the build log:

- name: Show .NET environment
  run: dotnet --info

Continue with the normal pipeline:

- name: Restore
  run: dotnet restore

- name: Build
  run: dotnet build --configuration Release --no-restore

- name: Test
  run: dotnet test --configuration Release --no-build

And publish the deployable artifact:

- name: Publish
  run: dotnet publish --configuration Release --no-build

The same principle applies to Azure DevOps, GitLab CI, Jenkins, TeamCity, or another build system:

Make the SDK version explicit and observable.

Do not rely on whatever SDK happens to exist on the build agent.


9. Update Your Container Images

Containerized applications introduce another version boundary.

A project can target .NET 10 while its Dockerfile still references an older image.

For an ASP.NET Core application, a simple multi-stage Dockerfile can look like this:

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src

COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /app/publish --no-restore

FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app

COPY --from=build /app/publish .

ENTRYPOINT ["dotnet", "MyApp.dll"]

Build the actual image:

docker build -t myapp:net10 .

Then run it:

docker run --rm -p 8080:8080 myapp:net10

Test that container—not just dotnet run.

There is another operational detail here.

Container tags such as 10.0 can move as patched images are released. That helps with servicing, but it also means reproducibility and security patching must be handled deliberately.

In stricter environments, consider controlling the exact image or digest through your release process and updating it intentionally when servicing updates are validated.


10. Verify the Runtime Inside the Container

Never infer the runtime version from the Dockerfile alone.

Inspect the image you actually built:

docker run --rm myapp:net10 dotnet --info

or:

docker run --rm myapp:net10 dotnet --list-runtimes

You want evidence that the artifact you intend to deploy contains the expected runtime.

This distinction becomes particularly important during security servicing.

Updating the SDK on a developer machine does not update:

  • an already-built Docker image
  • a deployed container
  • a runtime installed on a VM
  • a self-contained published artifact

Each deployable artifact has its own servicing path.


11. Record the Runtime at Application Startup

For important services, recording the framework version during application startup makes deployment verification much easier.

For example:

using System.Runtime.InteropServices;

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.Logger.LogInformation(
    "Starting application. Framework: {Framework}",
    RuntimeInformation.FrameworkDescription);

app.Run();

After deployment, your logs should show the runtime used by the process.

For example:

Starting application. Framework: .NET 10.0.x

This answers an operationally important question:

What is actually running in production?

Do not expose unnecessary environment information through a public endpoint. Internal diagnostics, deployment metadata, or structured startup logging are safer choices.


12. Test Production-Like Dependencies

Unit tests are necessary, but they are not enough for this migration.

Your integration suite should exercise the components most likely to behave differently across framework upgrades:

  • HTTP pipeline
  • authentication and authorization
  • serialization
  • database queries
  • transactions
  • caching
  • background workers
  • external API clients
  • file operations
  • date and time handling
  • configuration
  • dependency injection

If production uses SQL Server, an integration suite that replaces the database with an in-memory implementation cannot validate actual SQL translation.

Likewise, if production runs Linux containers but all testing happens only on Windows, you are leaving an important difference untested.

The closer your final migration test is to the production topology, the more useful it becomes.


13. Establish Performance Baselines Before Deployment

Do not benchmark every method in the application.

Identify the operations that actually matter:

  • high-volume API endpoints
  • expensive database queries
  • startup
  • background processing
  • serialization-heavy paths
  • memory-sensitive jobs

Record the current .NET 8 or .NET 9 baseline.

For example:

Endpoint             p50      p95      Error rate
-------------------------------------------------
GET /orders          45 ms    120 ms   0.1%
POST /checkout       90 ms    240 ms   0.2%
GET /customers       30 ms     80 ms   0.1%

After migration, compare the same workload.

The purpose is not to prove that .NET 10 is faster.

The purpose is to detect whether your application became slower.

Those are very different questions.


14. Deploy Gradually

Do not combine a framework upgrade with an unrelated architectural release if you can avoid it.

Ideally, the .NET 10 deployment contains only changes necessary for the migration.

Then deploy through your normal staged process:

Development
    ↓
CI
    ↓
Integration
    ↓
Staging
    ↓
Canary / small production slice
    ↓
Production

During rollout, watch:

  • HTTP 5xx rate
  • latency
  • CPU
  • memory
  • GC behavior
  • database load
  • connection pools
  • exceptions
  • background job failures
  • external API failures

A framework migration should have an explicit observation window.

Do not deploy, see green health checks for two minutes, and declare victory.


15. Have a Rollback You Can Actually Execute

Before deployment, answer this question:

If .NET 10 behaves badly in production, how long will it take us to return to the previous version?

For a containerized application, rollback may be as simple as redeploying the previous immutable image.

For example:

myapp:2026.08.14-net10
myapp:2026.08.01-net8

But database migrations can complicate rollback.

If the .NET 10 release contains a destructive schema migration, restoring the old application image may not restore compatibility.

That is why framework migrations and destructive database changes should usually be separated when possible.

Your rollback plan should cover:

  • previous application artifact
  • previous container image
  • database compatibility
  • configuration compatibility
  • deployment procedure
  • expected rollback time

A rollback plan that exists only in someone’s head is not a rollback plan.


16. Verify Production After Deployment

This is the step teams most often underweight.

After deployment, verify the deployed system—not merely the pipeline that produced it.

Confirm:

✓ Correct application version
✓ Correct .NET runtime
✓ Correct container image
✓ Expected configuration
✓ Database connectivity
✓ Authentication
✓ Critical endpoints
✓ Background workers
✓ Logging and telemetry
✓ No unexpected increase in errors

Then run a small production smoke test.

For example:

curl -f https://your-api.example.com/health

A useful health check should validate enough infrastructure to detect a broken deployment without turning every optional dependency into a reason for the entire service to report unhealthy.


Production Migration Checklist

Use this before calling the migration complete.

Before Changing Code

  • Record current SDK and runtime versions
  • Inventory all target frameworks
  • Inventory direct and important transitive NuGet dependencies
  • Record current container images
  • Record the CI SDK version
  • Establish performance baselines for critical paths
  • Review relevant .NET 10 breaking changes

During the Migration

  • Install a validated .NET 10 SDK
  • Add or update global.json
  • Change the target framework to net10.0
  • Restore dependencies
  • Resolve warnings and compilation failures deliberately
  • Upgrade framework-coupled packages
  • Review EF Core breaking changes
  • Inspect critical generated SQL
  • Run unit tests
  • Run integration tests
  • Publish in Release configuration

CI and Containers

  • Configure CI to use .NET 10 explicitly
  • Log dotnet --info in CI
  • Update the SDK container image
  • Update the ASP.NET Core/runtime container image
  • Build the production Docker image
  • Test the actual container
  • Verify the runtime inside the image

Before Production

  • Test in a production-like environment
  • Compare critical performance metrics
  • Prepare the previous artifact or image
  • Confirm database rollback compatibility
  • Define production observation metrics
  • Document the rollback procedure

After Deployment

  • Verify the deployed application version
  • Verify the deployed .NET runtime
  • Run smoke tests
  • Check error rates
  • Check latency
  • Check CPU and memory
  • Check database performance
  • Check background services
  • Keep monitoring throughout the observation window

The Most Important Lesson

A .NET upgrade has three different definitions of success.

The first is:

It compiles.

The second is:

The tests pass.

The third is:

The production application is running the expected .NET version,
its critical workflows still behave correctly,
and its operational metrics remain healthy.

Only the third one completes the migration.

.NET 8 and .NET 9 reaching end of support on November 10, 2026 gives teams a concrete deadline.

But there is little benefit in waiting until the deadline becomes urgent.

Move to .NET 10 while you still have enough time to test dependencies, inspect database behavior, validate the deployment artifact, and roll out gradually.

Changing the target framework takes a minute. Spend your engineering effort proving that everything around it still works.

References

Author

Write A Comment

Exit mobile version