Hướng dẫn sử dụng và tạo công cụ .NET hiệu quả

Trong bài viết này, tôi sẽ mô tả những phức tạp xung quanh việc tạo công cụ .NET, đặc biệt khi bạn không biết phiên bản runtime .NET nào sẽ được cài đặt trên máy của người dùng. Cuối cùng, tôi sẽ cung cấp một số mẹo để làm việc và kiểm tra công cụ .NET trong môi trường tích hợp liên tục (CI).

Công cụ .NET là gì?

Công cụ .NET là các chương trình được phân phối qua NuGet và có thể được cài đặt bằng .NET SDK. Chúng có thể được cài đặt toàn cục trên máy hoặc cục bộ cho một thư mục cụ thể.

Có một số công cụ toàn cục chính thức từ Microsoft, như công cụ EF Core, nhưng bạn cũng có thể tự viết công cụ của riêng mình. Đa số công cụ .NET là các công cụ dòng lệnh, nhưng không có lý do gì chúng cần phải như vậy. Ví dụ, công cụ Content Builder của MonoGame bao gồm cả công cụ GUI.

Làm việc với công cụ cục bộ

Một số công cụ phù hợp nhất khi được sử dụng như công cụ toàn cục. Nếu chúng áp dụng rộng rãi cho nhiều ứng dụng và bạn thường không cần phiên bản cụ thể của công cụ cho các dự án khác nhau (ví dụ: DiffEngineTray) thì công cụ toàn cục là lựa chọn hợp lý. Tuy nhiên, không phải lúc nào cũng vậy.

Trong những trường hợp này, công cụ cục bộ là một lựa chọn tốt hơn. Bạn có thể định nghĩa các công cụ cần thiết cho một dự án cụ thể bằng cách tạo dotnet-tools manifest. Đây là một tệp JSON nằm trong kho lưu trữ của bạn và được đưa vào kiểm soát mã nguồn. Bạn có thể tạo một manifest công cụ mới bằng cách chạy lệnh sau trong thư mục gốc của kho lưu trữ:

dotnet new tool-manifest

Theo mặc định, lệnh này tạo tệp manifest JSON dotnet-tools.json bên trong thư mục .config của kho lưu trữ:

{
  "version": 1,
  "isRoot": true,
  "tools": { }
}

Manifest ban đầu không bao gồm bất kỳ công cụ nào, nhưng bạn có thể cài đặt các công cụ mới bằng cách chạy dotnet tool install (tức là không có cờ -g hoặc --tool-path). Ví dụ, bạn có thể cài đặt công cụ Cake cho dự án của mình bằng cách chạy:

> dotnet tool install Cake.Tool

Lệnh này cập nhật manifest bằng cách thêm tham chiếu cake.tool vào phần tools, bao gồm phiên bản yêu cầu và lệnh bạn cần chạy để thực thi công cụ (dotnet-cake).

Khi một đồng nghiệp clone kho lưu trữ và muốn chạy công cụ Cake, họ có thể chạy các lệnh sau để khôi phục công cụ, sau đó chạy nó:

# Khôi phục các gói NuGet được chỉ định trong manifest
dotnet tool restore

# Chạy công cụ bằng một trong các hình thức sau:
dotnet tool run dotnet-cake
# hoặc bạn có thể sử dụng:
dotnet dotnet-cake
# hoặc thậm chí ngắn hơn:
dotnet cake

Ngoài ra, kể từ .NET 10 preview 6, bạn có thể sử dụng công cụ dnx hoặc dotnet dnx đơn giản hơn để chạy công cụ một lần. Khi công cụ được chỉ định trong manifest, bạn có thể chỉ cần sử dụng dnx và nó sẽ tự động sử dụng phiên bản được chỉ định trong manifest:

# Chạy công cụ một lần bằng một trong các lệnh sau:
dnx Cake.Tool
dotnet dnx Cake.Tool

Công cụ .NET về cơ bản chỉ là ứng dụng .NET được đóng gói vào gói NuGet, vì vậy chúng tuân theo tất cả các yêu cầu tương tự. Một trong những khía cạnh quan trọng nhất là thực tế các ứng dụng .NET được biên dịch cho một runtime cụ thể. Và đó cũng là nơi mọi thứ có thể trở nên hơi phức tạp.

Đảm bảo tương thích bằng cách đa mục tiêu

Có một vài khó khăn hơi khó chịu khi làm việc và tạo công cụ .NET. Công cụ .NET chỉ là các ứng dụng .NET phụ thuộc framework thông thường, vì vậy chúng phụ thuộc vào runtime .NET chính xác có sẵn trên máy của bạn. Ví dụ cụ thể, nếu bạn xây dựng một công cụ .NET và nó nhắm mục tiêu net8.0, thì bạn phải có runtime .NET 8 được cài đặt trên máy đích, bất kể phiên bản SDK nào bạn cài đặt công cụ.

Do đó, nếu bạn muốn hỗ trợ bất kỳ runtime nào mà khách hàng có thể đã cài đặt trên máy của họ, thì bạn cần xây dựng và đóng gói công cụ của mình cho nhiều framework đích.

Về nguyên tắc, điều này đủ dễ dàng, vì bạn có thể “chỉ” thêm tất cả các framework đích mà bạn cần hỗ trợ vào phần tử <TargetFrameworks> trong tệp .csproj:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <!-- 👇 Nhắm mục tiêu TẤT CẢ các framework! -->
    <TargetFrameworks>netcoreapp2.1;netcoreapp3.0;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0</TargetFrameworks>
    <LangVersion>latest</LangVersion>
    <PackAsTool>true</PackAsTool>
    <ToolCommandName>sayhello</ToolCommandName>
  </PropertyGroup>
</Project>

Như bạn có thể thấy từ danh sách trên, nếu bạn thực sự muốn hỗ trợ mọi thứ, thì đó là rất nhiều framework đích để thêm. Và điều này không phải không có nhược điểm của nó.

Đầu tiên, khi bạn tạo các ứng dụng đa mục tiêu như thế này, bạn sẽ nói chung bị giới hạn chỉ các API có trong framework đích thấp nhất. Trong ví dụ trên, điều đó có nghĩa là các API .NET Core 2.1😬

Hơn nữa, mỗi framework đích bạn thêm ở đây sẽ làm tăng kích thước của gói NuGet. Khi bạn đóng gói ứng dụng, bạn sẽ xây dựng nó cho từng framework đích và đóng gói mọi thứ vào cùng một gói NuGet:

Điều này có thể làm tăng đáng kể kích thước của gói, điều này nói chung không phải là vấn đề, ngoại trừ việc nó làm cho tất cả các thao tác khôi phục và dnx (ví dụ) chậm hơn.

Xây dựng và đóng gói cho tất cả các runtime đích mà bạn hỗ trợ là cách tiếp cận “an toàn nhất” để hỗ trợ phạm vi khách hàng rộng nhất có thể. Tuy nhiên, nếu bạn sẵn sàng chấp nhận một chút rủi ro thì có một cách tiếp cận thay thế.

Cấu hình công cụ của bạn để roll forward

Trong phần trước, tôi đã nói rằng việc nhắm mục tiêu rõ ràng tất cả các runtime .NET mà bạn hỗ trợ trong một công cụ .NET là cách “tốt nhất” để đảm bảo công cụ của bạn có thể chạy trên môi trường của khách hàng, bất kể họ sử dụng runtime nào.

Tuy nhiên, một cách tiếp cận thay thế (và theo nhiều cách, bổ sung) là không nhắm mục tiêu tất cả các phiên bản runtime này. Thay vào đó, bạn cho phép ứng dụng của mình chạy với phiên bản runtime mới hơn so với phiên bản nó được xây dựng, bằng cách sử dụng phần tử <RollForward>.

Ví dụ: giả sử bạn có một công cụ hoạt động trên .NET 6 và bạn không muốn phải đa mục tiêu nó cho .NET 7, .NET 8, .NET 9, v.v. Với khả năng tương thích rất cao của mỗi phiên bản .NET với phiên bản trước đó, bạn có thể thay vào đó chỉ xây dựng công cụ của mình cho .NET 6, và sau đó nói với máy chủ dotnet cho phép sử dụng bất kỳ runtime nào có sẵn cho .NET 6 trở lên. Bạn có thể làm điều này bằng cách đặt RollForward=Major trong tệp dự án của mình:

<PropertyGroup>
  <TargetFramework>net6.0</TargetFramework>
  <RollForward>Major</RollForward>
</PropertyGroup>

Việc đặt <RollForward> trong dự án của bạn đảm bảo rằng thuộc tính này được sao chép vào tệp runtimeonfig.json được triển khai cùng với công cụ của bạn. Bạn có thể tìm thấy điều này bên trong gói NuGet của mình; lưu ý thuộc tính rollFoward bên dưới phản ánh giá trị bạn đặt trong dự án của mình:

{
  "runtimeOptions": {
    "tfm": "net6.0",
    "rollForward": "Major",
    "framework": {
      "name": "Microsoft.NETCore.App",
      "version": "6.0.0"
    },
    "configProperties": {
      "System.Reflection.Metadata.MetadataUpdater.IsSupported": false
    }
  }
}

Với rollForward được cấu hình cho công cụ của bạn, miễn là ai đó có thể cài đặt công cụ .NET của bạn (mà họ có thể làm miễn là họ có .NET 6+ SDK được cài đặt) thì họ sẽ có thể chạy ứng dụng, mặc dù bạn chỉ xây dựng và đóng gói ứng dụng của mình cho .NET 6.

Lưu ý: Điều này không hoàn toàn an toàn, vì runtime .NET không được đảm bảo tương thích giữa các phiên bản chính. Tuy nhiên, trong thực tế, nó tương đối an toàn và thường được khuyến nghị.

Một trong những lý do tốt nhất để đặt RollForward=Major trong dự án của bạn ngay cả khi bạn đóng gói cho nhiều framework đích là để hỗ trợ các phiên bản .NET hiện chưa được phát hành sẽ ra mắt trong tương lai. Ví dụ: giả sử bạn có một công cụ .NET đã xuất bản hỗ trợ .NET 9. Theo mặc định, khi .NET 10 ra mắt, mọi người sẽ không thể chạy công cụ của bạn trừ khi bạn quay lại và thêm rõ ràng một mục tiêu net10.0. Bằng cách đặt RollForward=Major, bạn có thể đảm bảo có một số hỗ trợ ngay lập tức.

Mẹo hữu ích với dotnet tool

Phần cuối cùng trong bài viết này là một bộ sưu tập các tùy chọn hữu ích có sẵn khi làm việc với công cụ .NET, đặc biệt khi bạn đang thực hiện các công việc trong hệ thống tích hợp liên tục (CI). Đây thường là những điều tôi đã gặp phải khi tự làm việc với chúng và chúng không phải lúc nào cũng rõ ràng.

Kiểm tra các gói được xây dựng cục bộ với --source--tool-path

Như đã đề cập trước đó, công cụ .NET về cơ bản chỉ là các ứng dụng .NET, vì vậy phần lớn bạn có thể kiểm tra chúng theo cách bạn kiểm tra bất kỳ ứng dụng nào khác. Tuy nhiên, bạn cũng có thể muốn kiểm tra rõ ràng artifact cuối cùng mà bạn đang tạo, tức là tệp .nupkg.

Khi bạn đang kiểm tra một công cụ bạn đã tạo cục bộ, tôi khuyên bạn nên sử dụng cả cài đặt --source--tool-path:

  • --source Chỉ định nơi cài đặt công cụ từ đó. Chỉ nó đến một thư mục chứa tệp .nupkg để chỉ cài đặt từ các gói đó, thay vì các nguồn NuGet khác.
  • --tool-path Chỉ định nơi cài đặt công cụ vào đó. Công cụ .NET sẽ được cài đặt và giải nén vào thư mục này và sau đó có thể được chạy từ thư mục này.

Ví dụ:

# Cài đặt phiên bản 1.2.3 của gói dd-trace 
# được tìm thấy trong thư mục /app/install/
# và cài đặt nó vào đường dẫn /tool
dotnet tool install dd-trace \
    --source /app/install/. \
    --tool-path /tool \
    --version 1.2.3

Sử dụng cả hai cài đặt này khi bạn đang kiểm tra các gói được xây dựng cục bộ đảm bảo rằng bạn vừa thực sự cài đặt công cụ mà bạn nghĩ bạn đang cài đặt (thay vì vô tình cài đặt từ một nguồn từ xa), và rằng bạn không “làm ô nhiễm” bộ nhớ cache NuGet cục bộ của mình với các tệp kiểm tra này.

Cài đặt phiên bản pre-release với --prerelease

Nếu bạn đang tạo một gói có hậu tố pre-release (tức là nó có phiên bản như 1.0.0-beta hoặc 0.0.1-preview thay vì chỉ 1.0.0 hoặc 0.0.1) thì bạn có thể ngạc nhiên khi thấy rằng bạn không thể dễ dàng kiểm tra nó cục bộ. Điều này là do bạn phải truyền cờ --prerelease khi cài đặt phiên bản pre-release:

# Cờ --prerelease là bắt buộc khi cài đặt phiên bản pre-release
dotnet tool install dd-trace \
    --source /app/install/. \
    --tool-path /tool \
    --version 1.2.3-preview \
    --prerelease

Lưu ý rằng cờ này chỉ khả dụng từ .NET 5+ của .NET SDK.

Cung cấp tính mạnh mẽ với --allow-downgrade

Nếu bạn đang cài đặt một công cụ .NET trong CI, bạn thường nên chỉ định một phiên bản, để đảm bảo rằng CI của bạn có thể lặp lại. Nhưng điều gì sẽ xảy ra nếu công cụ đã được cài đặt?

> dotnet tool install -g dotnet-serve --version 1.10.175
The requested version 1.10.175 is lower than existing version 1.10.190.

Như được hiển thị trong ví dụ trên, nếu bạn cố gắng cài đặt một phiên bản của công cụ thấp hơn phiên bản hiện đang được cài đặt, điều này sẽ thất bại.

Lệnh dotnet tool update hoạt động bằng cách gỡ cài đặt công cụ và sau đó cài đặt phiên bản mới, vì vậy bạn có thể nghĩ rằng bạn có thể sử dụng nó thay thế, nhưng không:

> dotnet tool update -g dotnet-serve --version 1.10.175
The requested version 1.10.175 is lower than existing version 1.10.190.

Bạn nhận được thông báo lỗi chính xác giống nhau. Điều quan trọng ở đây là bạn cần bao gồm tùy chọn --allow-downgrade khi chạy dotnet tool update:

> dotnet tool update -g dotnet-serve --version 1.10.175 --allow-downgrade
Tool 'dotnet-serve' was successfully updated from version '1.10.190' to version '1.10.175'.

Lưu ý: dotnet tool install --allow-downgrade cũng hoạt động. Có vẻ như hai lệnh này ngày nay làm chính xác cùng một việc, vì vậy tôi không biết tại sao update vẫn chưa bị loại bỏ để thành thật😅

Tóm tắt

Trong bài viết này, tôi đã thảo luận về cách làm việc với công cụ .NET. Tôi đã mô tả cách cài đặt công cụ cục bộ bằng cách sử dụng manifest công cụ và một số cân nhắc khi bạn đang tạo công cụ. Đặc biệt, tôi đã thảo luận về các cân nhắc về việc đa mục tiêu để đảm bảo khả năng tương thích tối đa với môi trường của khách hàng và sử dụng RollForward=Major để đảm bảo khả năng tương thích trong tương lai. Cuối cùng, tôi đã cung cấp một số mẹo chung về sử dụng công cụ .NET, đặc biệt khi bạn đang xây dựng và kiểm tra công cụ .NET trong CI. Trong bài viết tiếp theo, chúng ta sẽ xem xét một số tính năng mới sắp ra mắt với công cụ .NET trong .NET 10!

Chỉ mục