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.
Table of Contents
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 --infoin 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.