Nếu bạn đang cần **nâng cấp ứng dụng từ .NET 8 lên .NET 10**, đừng xem đây chỉ là việc chỉnh sửa tệp dự án. Hãy coi đây là một quá trình **di dời hợp đồng API**. Một dịch vụ có thể biên dịch thành công, vượt qua các bài kiểm thử đơn vị, nhưng vẫn có thể gây bất ngờ cho người tiêu dùng với định dạng JSON, mã trạng thái, phản hồi xác thực hoặc tài liệu OpenAPI đã thay đổi.
Rủi ro này ngày càng trở nên quan trọng khi Microsoft đã chính thức xác nhận rằng [.NET 8 và .NET 9 sẽ kết thúc hỗ trợ vào ngày 10 tháng 11 năm 2026](https://devblogs.microsoft.com/dotnet/dotnet-8-9-end-of-support/). Hiện tại, .NET 10 và C# 14 là các bản phát hành ổn định mới nhất, và .NET 10 chính là đích đến LTS (Hỗ trợ dài hạn) được hỗ trợ chính thức.
Mục lục
Tại sao thời hạn ảnh hưởng đến thứ tự nâng cấp của tôi
Bước đầu tiên của tôi là **kiểm kê**, không phải là đổi target framework. Tôi liệt kê mọi dự án có thể triển khai, dự án kiểm thử, tệp `global.json`, ảnh cơ sở container, SDK được cố định trong CI, và tất cả các tham chiếu gói Microsoft. Lệnh `dotnet –list-sdks` cho biết máy có thể build những gì; `dotnet –info` cho thấy môi trường hiện tại thực sự giải quyết các phụ thuộc như thế nào.
Nếu kiểm kê cần chi tiết hơn, hướng dẫn cũ của tôi về [`dotnet sdk check`](https://dev.to/ssukhpinder/tracking-net-sdk-and-runtime-versions-with-dotnet-sdk-check-3f25) là một điểm khởi đầu hữu ích. Đối với các API vẫn đang chạy trên .NET 8, [danh sách kiểm tra thiết lập và bảo mật Web API tổng quát hơn](https://dev.to/ssukhpinder/mastering-net-8-web-api-from-setup-to-security-50-tips-guide-for-developers-n40) có thể giúp xác định các hành vi cần bảo vệ trước khi di chuyển.
Sau đó, tôi tách quá trình di dời thành ba thay đổi: SDK và Target Framework, các phụ thuộc NuGet, và cơ sở hạ tầng thời gian chạy (runtime infrastructure). Việc giữ các thay đổi này tách biệt giúp lỗi dễ xác định hơn. Một commit lớn kết hợp làm mới tất cả phụ thuộc có thể nhanh chóng tạo ra, nhưng rất khó để chẩn đoán khi có sự cố.
Nâng cấp .NET 8 lên .NET 10 phía sau các kiểm thử hợp đồng
Trước khi thay đổi `net8.0`, tôi thêm một bộ nhỏ các kiểm thử xung quanh các endpoint mà người tiêu dùng không thể chấp nhận thay đổi. Tôi quan tâm đến hành vi có thể quan sát được: mã trạng thái, loại nội dung, tên các trường JSON bắt buộc, và ranh giới xác thực. Tôi tránh việc khẳng định toàn bộ chuỗi serialized vì thứ tự thuộc tính vô hại có thể khiến kiểm thử đó trở nên ồn ào và không ổn định.
Dưới đây là một ví dụ kiểm thử xUnit tập trung cho một Minimal API:
csharp
using System.Net;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc.Testing;
using Xunit;
public sealed class ProductContractTests(
WebApplicationFactory
: IClassFixture
{
[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”);
// Kiểm tra mã trạng thái và định dạng phản hồi
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(
“application/json”,
response.Content.Headers.ContentType?.MediaType);
// Phân tích và kiểm tra cấu trúc JSON
await using var stream =
await response.Content.ReadAsStreamAsync();
using var json = await JsonDocument.ParseAsync(stream);
// Đảm bảo các trường hợp đồng quan trọng không đổi
Assert.Equal(
42,
json.RootElement.GetProperty(“id”).GetInt32());
Assert.True(json.RootElement.TryGetProperty(“name”, out _));
}
}
“`
Dự án kiểm thử tham chiếu `Microsoft.AspNetCore.Mvc.Testing` trong cùng dòng 10.0 với ứng dụng. Bài kiểm thử đơn lẻ này không phải là bộ kiểm tra tương thích hoàn chỉnh. Đây là một **mẫu模板** cho số lượng nhỏ các hợp đồng quan trọng nhất: một phép đọc thành công, một lỗi xác thực, một yêu cầu không được ủy quyền, và một phản hồi không tìm thấy. Các kiểm tra này sẽ bắt được các thay đổi hành vi mà kiểm thử đơn vị bên dưới pipeline HTTP không thể thấy được.
Đối với Minimal API ở cấp cao nhất (top-level), tôi cũng truy cập điểm vào cho dự án kiểm thử:
“`csharp
// Trong Program.cs của ứng dụng
app.Run();
// Để test có thể truy cập Program
public partial class Program
{
}
“`
Microsoft duy trì một [mẫu .NET 10 `WebApplicationFactory` đầy đủ](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/10.x/IntegrationTestsSample). Tôi sử dụng tài liệu này làm tham khảo cho thiết lập host kiểm thử thay vì tự tạo plumbing server tùy chỉnh.
Di chuyển Runtime, các gói và Pipeline cùng lúc
Với cơ sở kiểm tra đã ổn định, tôi tiến hành thay đổi target framework của dự án ứng dụng và kiểm thử thành `net10.0`. Tôi cập nhật các gói Microsoft.AspNetCore, Microsoft.Extensions và EF Core lên các bản phát hành 10.0 tương thích, sau đó tham khảo [danh mục các thay đổi gây phá vỡ (breaking changes) chính thức của .NET 10](https://learn.microsoft.com/en-us/dotnet/core/compatibility/10) thay vì suy đoán từ lỗi biên dịch.
“`xml
“`
Tôi chạy cùng một chuỗi lệnh kiểm tra ngắn在当地 và trong CI:
“`bash
dotnet package list –outdated
dotnet restore
dotnet build –warnaserror
dotnet test
dotnet publish -c Release
“`
Pipeline phải cài đặt SDK .NET 10, và các dịch vụ containerized phải sử dụng các ảnh build và runtime tương ứng 10.0. Việc chỉ cập nhật trên máy develop chứng minh rất ít về artifact sẽ đến tay môi trường production.
**OpenAPI xứng đáng có một quyết định riêng.** Trình tạo tích hợp sẵn trong ASP.NET Core 10 mặc định phát ra OpenAPI 3.1. Nếu trình tạo client hiện tại chỉ hiểu 3.0, tôi sẽ **tạm thời cố định định dạng này** và lên lịch thay đổi hợp đồng đó riêng:
“`csharp
builder.Services.AddOpenApi(options =>
{
options.OpenApiVersion =
Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_0;
});
“`
Việc cố định này là một công cụ tương thích, không phải là lý do để tránh OpenAPI 3.1 mãi mãi. Tôi giữ nó chỉ cho đến khi người tiêu dùng đã được kiểm tra và nâng cấp.
Khi nào tôi sẽ không sử dụng con đường nâng cấp trực tiếp này
Việc đổi target trực tiếp là sai lầm khi **nhà cung cấp cơ sở dữ liệu, thành phần xác thực, nền tảng hosting hoặc phụ thuộc thương mại chưa hỗ trợ .NET 10**. Trong trường hợp này, tôi tách biệt và giải quyết rào cản đó trước. Việc di chuyển qua .NET 9 có thể giúp hiển thị các thay đổi theo từng bước nhỏ hơn, nhưng .NET 9 cũng có cùng thời hạn hỗ trợ tháng 11 năm 2026, nên nó không phải là đích đến cuối cùng.
Tôi cũng **tránh kết hợp** việc nâng cấp runtime với việc thiết kế lại mô hình EF, viết lại xác thực, hoặc thay thế trình tạo OpenAPI. Tất cả đều có thể đáng giá, nhưng các commit và triển khai riêng biệt sẽ duy trì một ranh giới rollback hữu ích.
Đối với các thư viện chia sẻ, việc **tạm thời multi-target `net8.0;net10.0`** có thể cho phép các ứng dụng di chuyển độc lập. Tuy nhiên, đối với một API có thể triển khai, multi-targeting **không phải là sự thay thế** cho việc chọn và kiểm tra runtime mà production sẽ thực thi.
Kết luận
Việc nâng cấp từ .NET 8 lên .NET 10 là một bước đi cần thiết và khôn ngoan, nhưng nó phải được thực hiện có chiến lược. Bằng cách xem xét quá trình này như một cuộc di dời hợp đồng API, lập kiểm kê kỹ lưỡng, xây dựng lớp bảo vệ bằng kiểm thử hợp đồng, và thực hiện các thay đổi có kiểm soát, bạn có thể tận hưởng các tính năng và hiệu suất mới của .NET 10 mà không làm gián đoạn trải nghiệm của người dùng cuối.
Hợp đồng nào của API bạn sẽ cố định trước khi di chuyển dịch vụ của mình lên .NET 10? Hãy bắt đầu lên kế hoạch và thực hiện ngay hôm nay để đảm bảo ứng dụng của bạn luôn ổn định, an toàn và sẵn sàng cho tương lai. Chúc bạn nâng cấp thành công!



