The short answer: To upgrade to .NET 10 from .NET 8 or 9, install the
.NET 10 SDK, change your target framework moniker (TFM) to
net10.0, move your Microsoft packages to
10.0.x, rebuild, and fix what the compiler and your tests report. In an
ASP.NET Core Web API, that usually means the OpenAPI setup, a few EF Core
behavior changes, your Docker base images, and sometimes authentication
behavior. The timing matters too: Microsoft has announced that .NET 8 and .NET
9 both reach end of support on November 10, 2026.
Release notes list what changed, but they don't tell you which changes hit a typical Web API first. This guide upgrades an ASP.NET Core Web API (controllers, EF Core, Docker) from .NET 8 or 9 to .NET 10 and points out the changes Microsoft documents as breaking or obsolete. It doesn't cover moving an app from .NET Framework.
.NET 8 and .NET 9 reach end of support on the same day, November 10, 2026, according to Microsoft's .NET blog. Apps built on them keep running after that date, but technical support ends. .NET 10 is a Long Term Support (LTS) release, supported through November 2028.
We'll update the project files, check the OpenAPI setup, review the EF Core 10 behavior changes, update the Docker image, and scan for other documented changes. Then we'll put it all together in a complete example with a test and a CI step, and finish with the errors you're most likely to see.
The diagram below shows the order we'll follow. Each step maps to a section of this article, and the dashed line shows the loop you'll repeat when tests fail: fix the problem, rebuild, and test again.
Prerequisites
To follow along with this migration guide, you'll need:
-
The .NET 10 SDK installed on your development machine and build server. Run
dotnet --versionto confirm it prints a 10.0.x version. - An existing ASP.NET Core Web API on .NET 8 or .NET 9 that uses controllers, Entity Framework Core with SQL Server (the examples use it), and C#.
- An editor that supports .NET 10. Visual Studio 2026, released alongside .NET 10, is the straightforward choice, and Visual Studio Code with the C# Dev Kit extension also works. If you use Visual Studio 2022, check Microsoft's documentation before assuming it can target .NET 10.
- Docker, if you want to follow Step 4.
- A separate Git branch and a passing test suite, so you have a baseline to compare against.
You don't need any experience with .NET 10 previews, but you should be comfortable with NuGet package management and ASP.NET Core dependency injection.
Step 1: Updating Target Frameworks and Global Settings
Before you change anything, record what you have. These commands show which
SDK your folder resolves and which packages are outdated, deprecated, or
vulnerable. Run the
dotnet list package commands after a restore.
# Which SDK does this folder actually use?
dotnet --info
# Restore first, then audit your packages
dotnet restore
dotnet list package --outdated
dotnet list package --deprecated
dotnet list package --vulnerable
Now update the target framework moniker (TFM) in your project file (.csproj) from net8.0 or
net9.0 to
net10.0. If you pin the SDK with a
global.json file at the solution root, update
that too.
{
"sdk": {
"version": "10.0.100",
"rollForward": "latestFeature"
}
}
The rollForward value
latestFeature tells the CLI to use a newer
installed 10.0 feature band if the exact
10.0.100 SDK isn't present. That's handy on
build servers that have a slightly newer SDK.
Next, open each class library and Web API project file and update the
<TargetFramework> property. At the same
time, move your Microsoft package references to their 10.0.x releases so you
don't get version-mismatch warnings. The
10.0.0 below is only a starting point; use
the latest 10.0 patch version on NuGet.
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
</ItemGroup>
The
Microsoft.EntityFrameworkCore.Design package
is only needed for dotnet ef commands such as
migrations, which is why it's marked
PrivateAssets="all".
If your solution uses Central Package Management (CPM), you don't edit
versions in each project. Change them once in the
Directory.Packages.props file at the solution
root, and the project files keep their
PackageReference entries without a
Version attribute. You can recognize CPM by
the ManagePackageVersionsCentrally property
in that file.
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
</ItemGroup>
</Project>
With CPM turned on, putting a
Version attribute back on a
PackageReference in a project file causes a
restore error, so keep versions in this one file.
Now run dotnet restore and then
dotnet build. The build should finish with
"Build succeeded," usually with some warnings. Those warnings are your to-do
list: they point to obsolete APIs and missing or mismatched dependencies. Read
them before moving on.
You can retarget straight from .NET 8 to .NET 10; there's no technical requirement to stop at .NET 9. The catch is that you'll need to review the breaking changes for both releases at once.
Step 2: Upgrading ASP.NET Core OpenAPI Configuration
Built-in OpenAPI support (AddOpenApi and
MapOpenApi) arrived in .NET 9, when
Swashbuckle was dropped from the Web API project templates. So if you're
already on .NET 9 with the built-in support, the registration code below
doesn't change. If you're coming from .NET 8 and Swashbuckle, this is the
setup you'd move to.
What changed in .NET 10 is underneath. OpenAPI documents now default to
OpenAPI 3.1, and the OpenAPI.NET library behind them moved to version 2.0,
which breaks some custom document, operation, and schema transformers. Also,
the WithOpenApi extension method is
deprecated and produces the warning
ASPDEPR002. Microsoft's guidance is to remove
those calls and use
AddOpenApiOperationTransformer instead.
Here are the two parts of Program.cs that
matter for OpenAPI. The complete file appears in the example later in this
article.
// Built-in OpenAPI registration (available since .NET 9)
builder.Services.AddOpenApi(options =>
{
options.AddDocumentTransformer((document, context, cancellationToken) =>
{
document.Info ??= new();
document.Info.Title = "Codingvila Enterprise API";
document.Info.Version = "1.0.0";
return Task.CompletedTask;
});
});
if (app.Environment.IsDevelopment())
{
// Serves the OpenAPI JSON document (no UI is included)
app.MapOpenApi();
}
Run the app in the Development environment and open
/openapi/v1.json. You should see a JSON
document with the title "Codingvila Enterprise API." Note that
MapOpenApi only serves the document. If you
want a browser UI, add a separate tool such as Swagger UI or Scalar.
If you have custom transformers that compiled on .NET 9 but fail on .NET 10, check them against the OpenAPI.NET 2.0 types first. That's the most likely cause.
Step 3: Handling Entity Framework Core 10 Breaking Changes
EF Core 10 has a short list of documented breaking changes, and most are low impact. The ones most likely to affect a SQL Server Web API are:
-
Parameterized collections: a LINQ
Containsover a list is now translated with multiple scalar parameters by default instead of a single JSON array parameter. Queries still return the same rows, but performance can differ. You can useEF.Constanton the collection to change the translation for a specific query. -
JSON data type: if you configure EF with
UseAzureSqlor a compatibility level of 170 or higher, EF maps JSON columns to SQL Server's nativejsontype, which can show up in new migrations. -
ExecuteUpdate:
ExecuteUpdatenow accepts a regular (non-expression) lambda for the column setters. -
EF tools: if a project targets multiple frameworks with
<TargetFrameworks>, commands likedotnet ef migrations addnow require the--frameworkoption. -
SQLite: if you use Microsoft.Data.Sqlite, timestamps without an
offset are now treated as UTC when read as
DateTimeOffset.
To see how your own queries behave, turn on EF Core's SQL logging in a test environment after the upgrade and compare the generated SQL for your busiest queries with what you had on EF Core 8 or 9. The query in the complete example below is a good smoke test: it fetches active users with their five most recent orders from the last six months, and it pages the results.
If you want to try AsSplitQuery() on a query
like that, it's a performance choice, not an EF Core 10 requirement. It avoids
one large joined result set but runs more than one SQL query, so measure it
against your own data before you keep it.
Step 4: Updating Docker Containers and Deployment Pipelines
If your app runs in containers, change your Dockerfile to the .NET 10 SDK and
ASP.NET runtime images. If you skip this, your app won't start or your CI/CD
build will fail, because the app targets
net10.0 but the image still ships an older
runtime.
# Stage 1: Build the application
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY ["Codingvila.Api/Codingvila.Api.csproj", "Codingvila.Api/"]
RUN dotnet restore "Codingvila.Api/Codingvila.Api.csproj"
COPY . .
WORKDIR "/src/Codingvila.Api"
RUN dotnet publish "Codingvila.Api.csproj" -c Release -o /app/publish /p:UseAppHost=false --no-restore
# Stage 2: Run the application
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app
EXPOSE 8080
COPY --from=build /app/publish .
USER $APP_UID
ENTRYPOINT ["dotnet", "Codingvila.Api.dll"]
This is a two-stage build: the first stage compiles the app with the SDK
image, and the second copies only the published output into the smaller
runtime image. Copying the .csproj file and
restoring before copying the rest of the source lets Docker reuse the restore
layer when only your code changes. Add a
.dockerignore file that excludes
bin and
obj so local build output doesn't end up in
the image.
A few details are worth knowing:
-
Ubuntu images: the default
10.0tags now point to Ubuntu 24.04, and Microsoft doesn't ship Debian-based images for .NET 10. If your Dockerfile installs operating system packages withapt-get, test that they still work. -
Port 8080: .NET containers listen on port 8080 by default. I removed
EXPOSE 8081from the original because nothing in this app listens on 8081. -
Non-root user:
USER $APP_UIDruns the app as the non-root user that the .NET images provide, which limits the damage if the app is compromised. -
HTTPS redirection: inside a container that only listens on HTTP,
UseHttpsRedirectionlogs a warning that it couldn't determine the HTTPS port. If TLS ends at a proxy or ingress, remove the middleware or configure forwarded headers. If you already use forwarded headers, note thatForwardedHeadersOptions.KnownNetworksis obsolete in .NET 10. - Memory limits: set explicit memory limits in your container platform (Kubernetes manifests, ECS task definitions). The .NET runtime reads container limits when it sizes its memory use, so a declared limit gives you more predictable behavior.
Run docker build -t codingvila-api . from the
folder that contains the solution to confirm the build succeeds. When you run
the container, the logs should show the app listening on port 8080.
Other ASP.NET Core 10 Changes to Search For
Microsoft's ASP.NET Core 10 breaking-changes list includes more items than the ones above. Most only matter if you use that API, so search your solution for each name:
- Cookie login redirects: covered in the troubleshooting section below.
-
IActionContextAccessorandActionContextAccessorare obsolete. -
IPNetworkandForwardedHeadersOptions.KnownNetworksare obsolete. -
WebHostBuilder,IWebHost, andWebHostare obsolete. - Razor runtime compilation is obsolete.
-
The
IncludeOpenAPIAnalyzersproperty and the MVC API analyzers are deprecated. -
The
Microsoft.Extensions.ApiDescription.Clientpackage is deprecated. -
Exception diagnostics are suppressed when a custom exception handler's
TryHandleAsyncreturnstrue, which can change what your logs show.
Putting It Together: A Complete .NET 10 Web API Example
The snippets above are easier to trust when you can see them in one working
project. This example is a small Web API with one endpoint,
GET /api/users/active. It uses the project
file from Step 1 and the Dockerfile from Step 4. I haven't compiled it on
every setup, so build it on your own machine before relying on it.
Models and DbContext
Two entities, User and
Order, and a
DbContext that registers them. The
HasPrecision call tells SQL Server how many
digits to store for money, which avoids an EF Core warning about the decimal
type.
// Models/User.cs and Models/Order.cs
namespace Codingvila.Api.Models;
public class User
{
public int Id { get; set; }
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public bool IsActive { get; set; }
public List<Order> Orders { get; set; } = [];
}
public class Order
{
public int Id { get; set; }
public int UserId { get; set; }
public DateTime OrderDate { get; set; }
public decimal Total { get; set; }
public User User { get; set; } = null!;
}
// Data/AppDbContext.cs
using Codingvila.Api.Models;
using Microsoft.EntityFrameworkCore;
namespace Codingvila.Api.Data;
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
public DbSet<User> Users => Set<User>();
public DbSet<Order> Orders => Set<Order>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>().Property(o => o.Total).HasPrecision(18, 2);
}
}
In a real project, each class goes in its own file. They're combined here to keep the page short.
DTOs and the controller
The API returns small record types (DTOs) instead of entities. That keeps
database columns you didn't intend to expose out of your responses and avoids
circular references between User and
Order. The action also pages its results and
caps the page size, so one request can't pull your whole table.
// Dtos/UserDto.cs
namespace Codingvila.Api.Dtos;
public record OrderSummaryDto(int OrderId, DateTime OrderDate, decimal Total);
public record UserDto(int UserId, string FullName, List<OrderSummaryDto> RecentOrders);
// Controllers/UsersController.cs
using Codingvila.Api.Data;
using Codingvila.Api.Dtos;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Codingvila.Api.Controllers;
[ApiController]
[Route("api/users")]
public class UsersController(AppDbContext db) : ControllerBase
{
[HttpGet("active")]
public async Task<ActionResult<List<UserDto>>> GetActiveUsers(
int page = 1, int pageSize = 20, CancellationToken cancellationToken = default)
{
page = Math.Max(page, 1);
pageSize = Math.Clamp(pageSize, 1, 100);
var cutoff = DateTime.UtcNow.AddMonths(-6);
var users = await db.Users
.Where(u => u.IsActive)
.OrderBy(u => u.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(u => new UserDto(
u.Id,
u.FirstName + " " + u.LastName,
u.Orders
.Where(o => o.OrderDate >= cutoff)
.OrderByDescending(o => o.OrderDate)
.Take(5)
.Select(o => new OrderSummaryDto(o.Id, o.OrderDate, o.Total))
.ToList()))
.ToListAsync(cancellationToken);
return users;
}
}
Two small details matter here. The
cutoff date is computed once, so EF sends it
as a single parameter. And the OrderBy before
Skip and
Take makes paging predictable, since a page
without an order can return rows in any sequence.
Program.cs
This file wires everything together. It reads the connection string from
configuration instead of hard-coding it, turns on standard error responses,
and adds a health check endpoint. The empty
Program class at the bottom lets the test
project start the app.
using Codingvila.Api.Data;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddProblemDetails();
builder.Services.AddHealthChecks();
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(
builder.Configuration.GetConnectionString("Default")
?? throw new InvalidOperationException("Connection string 'Default' is missing.")));
builder.Services.AddOpenApi(options =>
{
options.AddDocumentTransformer((document, context, cancellationToken) =>
{
document.Info ??= new();
document.Info.Title = "Codingvila Enterprise API";
document.Info.Version = "1.0.0";
return Task.CompletedTask;
});
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
else
{
app.UseExceptionHandler();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.MapHealthChecks("/healthz");
app.Run();
public partial class Program { }
The connection string check sits inside the
AddDbContext lambda, so it runs the first
time something needs the database. That's why the health check test later in
this article can start the app without a database. If you'd rather fail at
startup, move the check out of the lambda.
Run it, migrate it, and test it
Keep the connection string out of source control. In development, store it
with user secrets. In production, set it as an environment variable named
ConnectionStrings__Default.
# One-time setup: the EF Core command-line tool (run "dotnet tool update" if you already have it)
dotnet tool install --global dotnet-ef
# Store the connection string outside your source code
dotnet user-secrets init --project Codingvila.Api
dotnet user-secrets set "ConnectionStrings:Default" "<your SQL Server connection string>" --project Codingvila.Api
# Create and apply the first migration
dotnet ef migrations add InitialCreate --project Codingvila.Api
dotnet ef database update --project Codingvila.Api
# Start the API, then open the URL printed in the console + /api/users/active
dotnet run --project Codingvila.Api
With an empty database,
/api/users/active returns
[]. Once you've added a user and an order,
you'll get JSON shaped like
[{"userId":1,"fullName":"Asha
Patel","recentOrders":[{"orderId":10,"orderDate":"2026-09-30T08:15:00","total":249.90}]}]. That sample is illustrative; your data will differ.
To run the container, pass the connection string as an environment variable and call the health endpoint:
docker build -t codingvila-api .
docker run --rm -p 8080:8080 -e ConnectionStrings__Default="<your SQL Server connection string>" codingvila-api
# In another terminal; this should print "Healthy"
curl http://localhost:8080/healthz
The container runs in the Production environment by default, so the OpenAPI document isn't served there, and you'll see the HTTPS redirection warning described in Step 4. Both are expected.
Finally, add a small integration test that starts the app in memory and checks the health endpoint. Tests like this are a cheap way to catch routing and startup problems after an upgrade. Create the test project with these commands:
dotnet new xunit -n Codingvila.Api.Tests
dotnet add Codingvila.Api.Tests package Microsoft.AspNetCore.Mvc.Testing --version "10.0.*"
dotnet add Codingvila.Api.Tests reference Codingvila.Api
dotnet test
using System.Net;
using Microsoft.AspNetCore.Mvc.Testing;
public class HealthEndpointTests(WebApplicationFactory<Program> factory)
: IClassFixture<WebApplicationFactory<Program>>
{
[Fact]
public async Task Health_endpoint_returns_200()
{
var client = factory.CreateClient();
var response = await client.GetAsync("/healthz");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
}
Running dotnet test should report one passing
test. As your suite grows, add tests for the endpoints your consumers can't
tolerate changing, so you notice contract changes right after an upgrade.
The last piece is your build pipeline, which must install the .NET 10 SDK too.
This GitHub Actions example pins the SDK, then restores, builds, and tests.
Run it from a repository where
dotnet restore can find a solution file, and
check for newer versions of the actions you use.
name: build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 10.0.x
- run: dotnet restore
- run: dotnet build --no-restore -c Release
- run: dotnet test --no-build -c Release
Common Errors and Troubleshooting
Error: MissingMethodException that mentions an OpenAPI method
This usually means the app is loading a mix of package versions, for example
an older OpenAPI-related package next to .NET 10 binaries. Align all
Microsoft.AspNetCore.* and related Microsoft
packages to the same 10.0.x version, then do a clean rebuild. Also check for
third-party OpenAPI packages (such as Swashbuckle) that haven't been updated
for .NET 10.
Warning: ASPDEPR002 on .WithOpenApi()
This is the deprecation warning described in Step 2. The code still compiles
and runs, but plan to remove the calls and move that logic into
AddOpenApiOperationTransformer.
Error: "The LINQ expression could not be translated"
This error isn't new in EF Core 10. It means EF Core can't turn part of your
query into SQL, and an upgrade can surface it if a query was only working by
accident. The fix is usually to move your own method call out of the query, or
to rewrite it using properties and operators EF Core can translate. If you
really need to run something in memory, call
AsEnumerable() only after the database has
already filtered the rows down to a small set.
Silent issue: API calls return 401 or 403 instead of redirecting to a login page
If your API uses cookie authentication, .NET 10 no longer redirects to the
login page for known API endpoints. That includes
[ApiController] endpoints, minimal API
endpoints that read or write JSON, and endpoints that return
TypedResults. They now return a 401 or 403
status code instead. For an API that's usually what you want, but front-end
code that relied on the redirect needs to handle the status code. If you need
the old behavior for one endpoint, Microsoft documents an
AllowCookieRedirect option, and an app-wide
switch for restoring it everywhere.
Warning: CS8618 (non-nullable property must contain a non-null value)
You'll see CS8618 when
<Nullable>enable</Nullable> is
set, as in the project file above, and a non-nullable property isn't
initialized in the constructor. Fix it by giving the property a default value
(for example = string.Empty;), marking it
with the required modifier, or making the
type nullable with ?. Use the null-forgiving
operator ! only when you're certain the value
will be set.
Compile error after moving to C# 14: a member named field
Projects targeting net10.0 use C# 14 by
default, which adds field as a keyword inside
property accessors. If one of your accessors refers to a member or variable
named field, rename it or write
@field or
this.field to keep the old meaning.
Best Practices for .NET 10 Upgrades
- Upgrade on a branch with a green baseline: run your tests before changing anything so you can tell new failures from old ones. Avoid combining the upgrade with an EF model redesign or an authentication rewrite.
-
Audit third-party packages: use
dotnet list package --outdatedand--deprecatedto find packages that haven't been updated for .NET 10. Look for maintained alternatives or replace the functionality yourself. -
Run integration tests: automated API tests catch changes in routing,
JSON serialization with
System.Text.Json, authentication behavior, and middleware order that the compiler can't. - Roll out gradually: deploy to staging first, and consider a canary or blue-green release so you can switch back quickly if something behaves differently in production.
-
Evaluate Native AOT separately: run
dotnet publish -p:PublishAot=trueto see what the analyzers report. Be aware that ASP.NET Core's Native AOT support doesn't cover MVC controllers, so the controller-based API in this article can't be published that way as-is. Treat AOT as its own project.
Frequently Asked Questions
Can I upgrade straight from .NET 8 to .NET 10?
Yes. You can retarget directly to net10.0.
The trade-off is that you'll review the breaking changes from both .NET 9 and
.NET 10 in one go, so keep the upgrade on its own branch.
Will my .NET 8 or .NET 9 app stop working on November 10, 2026?
No. Microsoft says apps built on them continue to run. What ends is technical support, so you should plan the move to .NET 10 before that date.
Is .NET 10 a Long Term Support release?
Yes. Microsoft lists .NET 10 as LTS, supported through November 2028.
Conclusion
To upgrade to .NET 10, work in a fixed order: baseline your tests and packages, update the TFM, check the OpenAPI setup, review the EF Core 10 changes, update your Docker image, and run your tests until they pass. Most of the work in a typical Web API is reading build warnings and the documented breaking changes, not rewriting code.
Key takeaways: .NET 8 and .NET 9 lose support on November 10, 2026; built-in
OpenAPI isn't new in .NET 10, but OpenAPI 3.1, OpenAPI.NET 2.0, and the
WithOpenApi deprecation are; EF Core 10's
changes are mostly low impact but worth checking against your SQL; and the
Ubuntu-based images and cookie authentication behavior are easy to miss. As a
next step, apply these changes to a non-critical service or a staging API
first, and compare behavior and performance before touching production.
