如果我需要升级 .NET 8 到 .NET 10,我将这项工作视为 API 契约迁移,而不是项目文件编辑。一项服务可能可以编译、通过单元测试,但仍然可能以改变的 JSON 结构、状态码、身份验证响应或 OpenAPI 文档的形式给消费者带来意外。

这种风险现在很重要,因为微软已经确认.NET 8 和 .NET 9 将于 2026 年 11 月 10 日结束支持。.NET 10 和 C# 14 是当前的稳定版本,而 .NET 10 是受支持的 LTS 目标。

为什么截止日期改变了我的升级顺序

我的第一步是盘点,而不是重新定位目标。我列出每一个可部署项目、测试项目、global.json、容器基础镜像、CI SDK 固定版本和 Microsoft 包引用。dotnet --list-sdks 显示一台机器可以构建什么;dotnet --info 显示当前环境实际解析的内容。

如果这份盘点需要更多细节,我之前关于dotnet sdk check的指南是一个有用的起点。对于仍在使用 .NET 8 的 API,更广泛的Web API 设置和安全检查清单可以帮助在迁移前识别需要保护的行为。

然后,我将迁移分为三个变更:SDK 和目标框架、NuGet 依赖项以及运行时基础架构。保持这些变更可见可以更容易定位故障。一个巨大的依赖刷新提交可能很快就能创建,但很难诊断。

在契约测试背后将 .NET 8 升级到 .NET 10

在更改 net8.0 之前,我围绕消费者不能容忍改变的端点添加一小部分测试。我关心可观察的行为:状态码、内容类型、必需的 JSON 名称和身份验证边界。我避免断言整个序列化字符串,因为无害的属性顺序可能使该测试变得嘈杂。

以下是针对 Minimal API 的一个重点 xUnit 测试:

using System.Net;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc.Testing;
using Xunit;

public sealed class ProductContractTests(
    WebApplicationFactory<Program> factory)
    : IClassFixture<WebApplicationFactory<Program>>
{
    [Fact]
    public async Task GetProduct_keeps_the_public_contract()
    {
        using var client = factory.CreateClient(new()
        {
            BaseAddress = new Uri("https://localhost")
        });

        using var response = await client.GetAsync("/products/42");

        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        Assert.Equal(
            "application/json",
            response.Content.Headers.ContentType?.MediaType);

        await using var stream =
            await response.Content.ReadAsStreamAsync();
        using var json = await JsonDocument.ParseAsync(stream);

        Assert.Equal(
            42,
            json.RootElement.GetProperty("id").GetInt32());
        Assert.True(json.RootElement.TryGetProperty("name", out _));
    }
}

Enter fullscreen mode Exit fullscreen mode

测试项目引用与应用程序相同的 10.0 服务线中的 Microsoft.AspNetCore.Mvc.Testing。这个测试不是一个完整的兼容性套件。它是针对最重要的少数契约的模板:成功的读取、验证失败、未授权请求和未找到的响应。这些检查可以捕获单元测试在 HTTP 管道之下无法看到的行为变化。

对于顶级 Minimal API,我还向测试项目公开入口点:

app.Run();

public partial class Program
{
}

Enter fullscreen mode Exit fullscreen mode

微软维护了一个完整的.NET 10 WebApplicationFactory 示例。我使用它作为测试主机设置的参考,而不是创建自定义服务器管道。

一起移动运行时、包和管道

在基线变为绿色后,我将应用程序和测试项目更改为 net10.0。我将 Microsoft.AspNetCore、Microsoft.Extensions 和 EF Core 包更新为兼容的 10.0 服务版本,然后查看官方的.NET 10 破坏性变更目录,而不是仅凭编译器错误猜测。

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

Enter fullscreen mode Exit fullscreen mode

我在本地和 CI 中运行相同的短序列:

dotnet package list --outdated
dotnet restore
dotnet build --warnaserror
dotnet test
dotnet publish -c Release

Enter fullscreen mode Exit fullscreen mode

管道必须安装 .NET 10 SDK,容器化服务必须使用匹配的 10.0 构建和运行时镜像。仅更新开发人员机器对到达生产环境的工件几乎没有证明作用。

OpenAPI 值得明确决定。ASP.NET Core 10 的内置生成器默认发出 OpenAPI 3.1。如果现有的客户端生成器只理解 3.0,我会临时固定格式并单独安排该契约变更:

builder.Services.AddOpenApi(options =>
{
    options.OpenApiVersion =
        Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_0;
});

Enter fullscreen mode Exit fullscreen mode

这种固定是一种兼容性工具,而不是永远避免 OpenAPI 3.1 的理由。我只在消费者经过测试和升级之前保留它。

我何时不会使用这种直接路径

当数据库提供程序、身份验证组件、托管平台或商业依赖项不支持 .NET 10 时,直接重新定位是错误的做法。在这种情况下,我首先隔离阻塞因素。通过 .NET 9 进行迁移可能有助于分步暴露更改,但 .NET 9 的支持截止日期也是 2026 年 11 月,因此它不是最终目标。

我还避免将运行时升级与 EF 模型重新设计、身份验证重写或 OpenAPI 生成器替换结合在一起。这些可能都值得做,但单独的提交和部署保留了一个有用的回滚边界。

对于共享库,临时的 net8.0;net10.0 多目标可以让应用程序独立迁移。对于可部署的 API,多目标不能替代选择和验证生产将执行的运行时。

在将服务迁移到 .NET 10 之前,您会固定哪个 API 契约?

感谢您的阅读 ? 下次见。