Versions, Support and Upgrades
Plan upgrades around LTS and STS support windows, keep runtime patches flowing into images, and move a service from net8.0 to net10.0.
<TargetFramework>net10.0</TargetFramework>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
FROM mcr.microsoft.com/dotnet/aspnet:10.0Every November brings a major release. Even-numbered releases are LTS and get 36 months of patches; odd-numbered releases are STS and get 24 months. Within a release, Microsoft ships a servicing update roughly monthly, usually on Patch Tuesday, and those patches carry the security fixes. "Supported" means on the latest patch: a service still on 10.0.2 is exposed to everything fixed since, even though .NET 10 itself is in support.
Support windows
| Release | Type | Released | End of support |
|---|---|---|---|
| .NET 8 | LTS | November 2023 | November 2026 |
| .NET 9 | STS | November 2024 | November 2026 |
| .NET 10 | LTS | November 2025 | November 2028 |
| .NET 11 (next release) | STS | November 2026 | 24 months later |
Most teams move from LTS to LTS and accept a two-year gap. Taking STS releases means an upgrade every year, in exchange for new runtime and library improvements earlier.
Target framework monikers
<TargetFramework>net10.0</TargetFramework> selects the API surface you compile against and the default C# version. OS-specific forms such as net10.0-windows add platform APIs, and <TargetFrameworks>net8.0;net10.0</TargetFrameworks> multi-targets a library while its consumers upgrade at different speeds. The TFM does not choose a patch. A framework-dependent app rolls forward to the newest installed patch of its major version, so it gets 10.0.13 by running on an image that has 10.0.13. A self-contained or Native AOT app carries the runtime inside and stays on the patch it was built with until rebuilt.
Keeping runtime images patched
The aspnet:10.0 tag moves with every patch, but running pods do not: they keep the layers they were built from. Rebuild and redeploy on a schedule (weekly, plus immediately for a critical advisory). Teams that pin images by digest for reproducibility need a bot such as Renovate or Dependabot to raise digest bumps, or the pin quietly freezes them on an old patch. Expose the running runtime version (an /about endpoint or a startup log line) so you can prove what is deployed. Expose /about only to internal callers (.RequireAuthorization("ops"), .RequireHost("*:9090") on a management port, or keep it off the public ingress), since an exact patch and OS description help an attacker fingerprint unpatched replicas; otherwise rely on the startup log line.
Upgrading a service from net8.0 to net10.0
- Install the 10.0 SDK and update
global.json. - Change the TFM, ideally once in
Directory.Build.props. - Move Microsoft packages from 8.0.x to the matching 10.0 versions (EF Core, authentication, extensions) and third-party packages to versions that support
net10.0. - Read the breaking-change pages for every release you skip: going from 8 to 10 means reading both the .NET 9 and .NET 10 lists, for the runtime, ASP.NET Core and EF Core.
- Build with warnings visible, fix new obsoletion and analyzer warnings, and run all tests.
- Change the Dockerfile base images. The .NET 10 default images are Ubuntu 24.04 rather than the Debian base of .NET 8, so check any
apt-getlines. - Update the CI SDK, deploy to staging and compare memory and latency with the old version before production.
Example
When to use it
- Plan the move of eight .NET 8 services to net10.0 before .NET 8 leaves support in November 2026.
- Prove in an incident review which runtime patch each pod was running by calling its /about endpoint.
- Rebuild every image weekly so framework-dependent APIs pick up the monthly runtime security patch without code changes.
More examples
A shared library that multi-targets during the migration
With <code>TargetFrameworks</code> (plural) the library builds once per framework and the NuGet package or project reference hands each consumer the closest match. Services can then upgrade one at a time instead of in one risky release. Keep conditional code to a minimum and drop the old target as soon as nothing uses it, or the library carries .NET 8 code past its end of support.
<!-- src/Shop.Contracts/Shop.Contracts.csproj: DTOs and events used by services on both versions -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<!-- Remove net8.0 once the last consuming service has moved to net10.0 -->
<PropertyGroup Condition="'$(TargetFramework)' == 'net10.0'">
<DefineConstants>$(DefineConstants);SHOP_NET10</DefineConstants>
</PropertyGroup>
</Project>Weekly rebuild to pick up runtime patches
<code>--pull</code> forces Docker to fetch the current <code>aspnet:10.0</code> layers instead of reusing a cached, older patch. Printing <code>--list-runtimes</code> leaves evidence in the build log of which patch shipped. The pushed image still has to be rolled out by the normal deployment pipeline; a rebuilt image sitting in a registry patches nothing.
name: rebuild-orders-api-image
on:
schedule:
- cron: "0 5 * * 3" # Wednesdays 05:00 UTC, the day after Patch Tuesday in most months
workflow_dispatch: {}
jobs:
rebuild:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Log in to the registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
- name: Build with freshly pulled base images
run: docker build --pull -t ghcr.io/shop/orders-api:patched-${{ github.run_number }} .
- name: Show the runtime inside the image
run: docker run --rm --entrypoint dotnet ghcr.io/shop/orders-api:patched-${{ github.run_number }} --list-runtimes
- name: Push
run: docker push ghcr.io/shop/orders-api:patched-${{ github.run_number }}Upgrade checks from the command line
Run the package listings after changing the TFM, because some packages only resolve a newer version once the project targets <code>net10.0</code>. <code>-warnaserror</code> surfaces new obsoletion and analyzer warnings introduced by the upgrade, which are cheap to fix now and expensive once someone suppresses them. The final command confirms the runtime patch level of the image you are about to ship.
# Which SDKs and runtimes this machine has
dotnet --list-sdks
dotnet --list-runtimes
# Point the repository at the new SDK band
dotnet new globaljson --sdk-version 10.0.402 --roll-forward latestFeature --force
# After changing TargetFramework: what is outdated or vulnerable now
dotnet package list --outdated
dotnet package list --vulnerable --include-transitive
# Build with every warning visible, then run the whole test suite
dotnet build -c Release -warnaserror
dotnet test -c Release
# What the new base image actually contains
docker run --rm --entrypoint dotnet mcr.microsoft.com/dotnet/aspnet:10.0 --list-runtimes
Discussion