Moving an existing application to a newer .NET version is usually less about rewriting application code and more about checking how the new runtime, SDK, libraries, and tooling affect the application you already have.
For a small application, changing the target framework may be straightforward. For a production application with multiple projects, external NuGet packages, database access, background workers, CI/CD pipelines, containers, and platform-specific dependencies, the migration needs a more careful approach.
A project can compile successfully after changing its target framework and still have problems at runtime. Authentication behavior, background services, API contracts, database access, serialization, deployment configuration, and third-party packages can all introduce migration issues.
That is why a .NET migration should be treated as an application compatibility exercise rather than simply changing one value in a project file.
Start by Checking the Existing Application
Before changing the target framework, first understand what the application currently depends on.
For example, a solution might contain several projects:
MyCompany.sln
├── Web
├── Application
├── Infrastructure
├── Worker
└── Tests
Review the following areas before starting the migration:
- Current target framework
- SDK version
global.json- NuGet package versions
- ASP.NET Core dependencies
- Entity Framework Core dependencies
- Authentication and authorization packages
- Database providers
- Background services
- Docker base images
- CI/CD build environments
- Native dependencies
- Platform-specific code
- Integration and end-to-end tests
This inventory is important because the migration affects more than the application runtime.
For example, if a project currently targets net10.0, changing it to net11.0 means the project needs to build and run against the corresponding .NET 11 framework and SDK.
The project change itself is simple:
<PropertyGroup>
<TargetFramework>net11.0</TargetFramework>
</PropertyGroup>
However, that should be considered the beginning of the migration, not the end.
Check the SDK and global.json
The .NET SDK controls much more than compilation.
It is involved in project evaluation, MSBuild execution, package restoration, testing, publishing, analyzers, and several command-line operations.
If the repository contains a global.json file, review it before starting the migration:
{
"sdk": {
"version": "11.0.100"
}
}
The exact SDK version should be the one selected by your development team and supported by your build environment.
Then verify the SDK installed on the development machine:
dotnet --info
dotnet --list-sdks
After that, perform a clean restore and build:
dotnet restore
dotnet build
dotnet test
Do the same validation in the CI environment.
A common problem during framework upgrades is having developers use one SDK while the CI server uses another. The project may work locally but fail during automated builds because project evaluation, analyzers, package resolution, or SDK behavior differs.
Review NuGet Packages Before Updating Everything
NuGet dependencies are often one of the biggest sources of migration work.
Changing the target framework does not automatically guarantee that every existing package is compatible with the new framework.
Start by reviewing the dependency tree:
dotnet list package
Pay particular attention to packages related to:
- ASP.NET Core
- Entity Framework Core
- Authentication
- Logging
- OpenTelemetry
- Serialization
- Database drivers
- Messaging
- Caching
- Testing
- Native libraries
Do not automatically upgrade every package to its newest version.
That approach introduces unnecessary changes and makes troubleshooting harder.
A better strategy is to identify which packages need to change because of the framework migration and upgrade those packages deliberately.
For example:
.NET framework migration
↓
Check package compatibility
↓
Upgrade required packages
↓
Restore
↓
Build and test
Keeping unrelated dependency changes out of the migration makes it easier to identify the source of a problem.
Understand the Different Types of Breaking Changes
One of the most important parts of a framework migration is understanding that not every breaking change produces a compiler error.
Some changes are detected immediately:
API changed
↓
Compilation error
↓
Fix code
↓
Build succeeds
Other changes affect runtime behavior:
Existing code
↓
Build succeeds
↓
Application starts
↓
Behavior changes
↓
Runtime problem
The second type is harder to detect.
For example, an application may compile successfully while an authentication flow, serialization behavior, HTTP response, middleware interaction, or background process behaves differently.
This is why integration testing is particularly important during a framework migration.
Review ASP.NET Core Changes Separately
If the application is an ASP.NET Core application, do not limit the migration review to the general .NET runtime.
ASP.NET Core has its own set of compatibility and behavior changes involving areas such as:
- Middleware
- Kestrel
- OpenAPI
- Authentication
- MVC
- Blazor
- Response compression
- Hosting
- HTTP behavior
These changes can affect applications even when the application code itself looks unchanged.
For example, if the application generates OpenAPI documents that are consumed by frontend applications, client SDK generators, API gateways, or external teams, verify the generated document after migration.
A successful API response does not necessarily mean the API contract is unchanged.
A useful migration test is:
API endpoint
↓
Generated OpenAPI document
↓
Schema validation
↓
Client generation
↓
Existing client tests
This catches compatibility problems that ordinary controller tests may not detect.
Pay Attention to Authentication and Middleware
Authentication is another area that deserves dedicated testing.
Do not test only the successful authentication path.
Test scenarios such as:
- Anonymous request
- Missing token
- Expired token
- Invalid token
- Missing role
- Missing policy requirement
- Multiple claims
- API-to-API authentication
- Cookie authentication, if used
For example:
HTTP Request
↓
Authentication
↓
Authorization
↓
Middleware
↓
Endpoint
↓
Response
Also review custom middleware ordering.
A framework upgrade is a good opportunity to verify that the actual middleware pipeline matches the application's intended architecture.
Problems in this area can be difficult to identify because the application may start normally while only certain requests fail.
Background Services Need Separate Testing
Applications that use BackgroundService or other hosted services should receive additional attention during migration.
A background worker may look simple:
public class OrderProcessor(
ILogger<OrderProcessor> logger) : BackgroundService
{
protected override async Task ExecuteAsync(
CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await ProcessOrders(stoppingToken);
await Task.Delay(
TimeSpan.FromSeconds(10),
stoppingToken);
}
}
private Task ProcessOrders(
CancellationToken cancellationToken)
{
logger.LogInformation("Processing orders");
return Task.CompletedTask;
}
}
The important part is not only whether the worker starts.
Test what happens when:
Database becomes unavailable
↓
Message broker becomes unavailable
↓
External API times out
↓
Unexpected exception occurs
↓
Cancellation is requested
↓
Application shuts down
A background process that stops unexpectedly can create serious operational problems without making the main API unavailable.
For applications that process payments, orders, notifications, messages, or scheduled jobs, background-worker behavior should be part of the migration test plan.
Check Runtime and Performance Behavior
A newer .NET runtime can contain improvements to areas such as JIT compilation, async execution, memory management, and other runtime operations.
That does not mean every application will automatically become faster.
Application performance is usually influenced by many layers:
Application code
↓
.NET runtime
↓
Database
↓
Network
↓
External services
↓
Infrastructure
A framework upgrade changes one important part of that system, but it does not eliminate bottlenecks elsewhere.
For this reason, establish performance baselines before migration.
For example:
Before migration
Requests/sec: 1,200
P95 latency: 180 ms
P99 latency: 420 ms
CPU: 48%
Memory: 1.4 GB
Error rate: 0.08%
After migration, collect the same measurements under comparable workloads.
The useful question is not:
"Is the new .NET version faster?"
Instead, ask:
"How did this application behave before and after the migration?"
That gives the team measurable evidence instead of relying on assumptions.
Check the Production Environment
The development machine is not the production environment.
Before deployment, verify the infrastructure that actually runs the application.
Check:
- Operating system
- CPU architecture
- Container runtime
- Memory limits
- Native libraries
- Certificates
- File permissions
- Environment variables
- Secret providers
- Cloud identity
- Network configuration
- Database connectivity
This becomes especially important for applications running on older servers, specialized infrastructure, self-hosted CI runners, or platform-specific environments.
A local build can succeed while the production environment is missing a required runtime dependency.
Update Docker Images
If the application runs inside containers, the Dockerfile must be reviewed as part of the migration.
A Dockerfile might currently contain:
FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY . .
ENTRYPOINT ["dotnet", "MyApp.dll"]
The runtime image should be changed to the appropriate .NET 11 image:
FROM mcr.microsoft.com/dotnet/aspnet:11.0
WORKDIR /app
COPY . .
ENTRYPOINT ["dotnet", "MyApp.dll"]
If the application uses a multi-stage Docker build, update the SDK image as well.
For example:
FROM mcr.microsoft.com/dotnet/sdk:11.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:11.0
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "MyApp.dll"]
After updating the Dockerfile, test the actual container.
Do not consider the migration complete just because dotnet run works on a developer machine.
Review the CI/CD Pipeline
The CI/CD pipeline is another part of the application.
A typical deployment path looks like this:
Developer machine
↓
Restore
↓
Build
↓
Test
↓
Container build
↓
Security checks
↓
Deployment
↓
Staging
↓
Production
Every stage needs to understand the new framework version.
Review:
- Build images
- SDK installation
- Test runners
- Docker images
- Deployment scripts
- Infrastructure definitions
- Environment configuration
- Package caching
- Artifact publishing
- Runtime health checks
A common migration mistake is updating the application repository while leaving the CI environment on the previous SDK or runtime.
The result can be confusing because the source code appears correct while the pipeline fails for infrastructure-related reasons.
Test the Database and External Integrations
A framework migration should also include integration testing.
If the application communicates with a database, message broker, cache, payment provider, storage service, or external API, test those integrations against realistic environments.
For database applications, verify:
Application startup
↓
Database connection
↓
Queries
↓
Transactions
↓
Migrations
↓
Connection pooling
↓
Error handling
Do not test only simple read operations.
Test transactions, timeouts, connection failures, retries, and application shutdown.
The same principle applies to external services.
A successful application startup tells you very little about how the application behaves when a dependency becomes slow or unavailable.
Avoid Combining Too Many Changes
One of the easiest ways to make a migration difficult is to change everything at the same time.
For example, avoid combining:
.NET upgrade
+
Database provider change
+
Major ORM upgrade
+
Authentication rewrite
+
Logging change
+
Infrastructure migration
If something breaks, finding the cause becomes unnecessarily difficult.
A more controlled migration looks like:
.NET framework
↓
Required package updates
↓
Application fixes
↓
Tests
↓
Infrastructure updates
↓
Performance validation
↓
Production deployment
Unrelated modernization work can be handled separately unless there is a strong technical reason to combine it.
A Practical Migration Process
A production application can follow this sequence:
- Create a dedicated migration branch.
- Verify the required .NET SDK.
- Review
global.json, if present. - Change the target framework to
net11.0. - Restore dependencies.
- Review NuGet package compatibility.
- Resolve compilation errors.
- Review .NET and ASP.NET Core compatibility changes.
- Run unit tests.
- Run integration and end-to-end tests.
- Update Docker images.
- Update CI/CD environments.
- Deploy to staging.
- Run functional and performance tests.
- Monitor logs, metrics, and traces.
- Deploy to production using the normal rollout process.
- Continue monitoring after deployment.
This approach makes it easier to isolate problems because each stage has a specific purpose.
Common Migration Mistakes
Treating a successful build as a successful migration
A successful build only confirms that the source code compiles.
It does not confirm that runtime behavior, integrations, infrastructure, or performance remain correct.
Updating every NuGet package
Unnecessary package upgrades increase the number of variables involved in the migration.
Upgrade what is required and validate each significant dependency change.
Forgetting Docker and CI/CD
The application can work locally while the production build still uses an older SDK or runtime image.
Infrastructure should be included in the migration plan.
Testing only the happy path
Authentication failures, database outages, background-worker exceptions, network timeouts, and graceful shutdown behavior often reveal problems that normal functional tests miss.
Skipping performance baselines
Without before-and-after measurements, it is difficult to determine whether the migration improved or degraded application behavior.
Final Thoughts
Moving an existing application to .NET 11 is usually not a rewrite. In many applications, the source-code changes are relatively small. The larger task is validating everything around that code.
The SDK, NuGet dependencies, ASP.NET Core behavior, authentication, background services, databases, containers, CI/CD pipelines, and production infrastructure all form part of the migration.
The most important distinction is between an application that builds and an application that continues to behave correctly.
A successful migration should therefore answer several questions:
Does it build?
Does it pass tests?
Does it behave correctly?
Do integrations still work?
Does it perform acceptably?
Does it deploy correctly?
Can the team monitor and roll it back safely?
If the answer to all of these is yes, the migration is much more than a target-framework change. It is a controlled upgrade of the application's runtime environment with the necessary validation around it.
Join the conversation! Your thoughts help the community grow.