如果需要從 .NET 8 升級至 .NET 10,我會將這項工作視為 API 合約遷移,而非僅僅是專案檔案的修改。一項服務可能可以編譯、通過單元測試,但仍然可能因 JSON 結構、狀態碼、驗證回應或 OpenAPI 文件的改變而讓消費者感到意外。

這個風險現在變得非常重要,因為 Microsoft 已確認 .NET 8 與 .NET 9 將於 2026 年 11 月 10 日結束支援。.NET 10 與 C# 14 是目前穩定的版本,而 .NET 10 是支援的 LTS 目標。

為什麼截止日期改變了我的升級順序

我的第一步是盤點,而非重新定位目標。我會列出所有可部署的專案、測試專案、global.json、容器基礎映像、CI SDK 固定版本,以及 Microsoft 套件參考。dotnet --list-sdks 可以顯示機器能建置哪些 SDK;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

Microsoft 維護了一份完整的 .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 合約?

感謝閱讀 ? 我們下次見。