.NET工具开发与使用全攻略

本文深入探讨.NET工具的创建与使用,涵盖本地工具管理、多目标框架兼容性配置、前滚策略设置,以及在CI环境中测试和安装工具的最佳实践技巧。

使用和创建.NET工具

在这篇文章中,我将描述创建.NET工具时遇到的一些复杂性,特别是当您不知道客户将安装哪个版本的.NET运行时的情况。最后,我将提供在持续集成(CI)环境中使用和测试.NET工具的一些技巧。

什么是.NET工具?

.NET工具是通过NuGet分发并可以使用.NET SDK安装的程序。它们可以全局安装到计算机上,也可以本地安装到特定文件夹中。微软提供了一些第一方全局工具,比如EF Core工具,但您也可以编写自己的工具。过去我描述过创建使用TinyPNG API压缩图像的工具,以及将web.config文件转换为appsettings.json格式的工具。

大多数.NET工具是命令行工具,但它们不一定必须是。例如,MonoGame的Content Builder工具也包括GUI工具。

使用本地工具

有些工具最适合作为全局工具。如果它们广泛适用于多个应用程序,并且您通常不需要为不同项目使用特定版本的工具(DiffEngineTray是一个很好的例子),那么全局工具是有意义的。

然而,情况并非总是如此。有时工具的版本确实很重要,您希望为不同的应用程序使用不同的版本。在这些情况下,本地工具是更好的选择。

您可以通过创建dotnet-tools清单来定义特定项目所需的工具。这是一个JSON文件,位于您的存储库中,并检入到源代码控制中。您可以通过在存储库的根目录中运行以下命令来创建新的工具清单:

1
dotnet new tool-manifest

默认情况下,这会在存储库的.config文件夹中创建以下清单JSON文件dotnet-tools.json:

1
2
3
4
5
{
  "version": 1,
  "isRoot": true,
  "tools": { }
}

初始清单不包含任何工具,但您可以通过运行dotnet tool install(即不带-g--tool-path标志)来安装新工具。例如,您可以通过运行以下命令为您的项目安装Cake工具:

1
2
3
4
> dotnet tool install Cake.Tool

您可以使用以下命令从此目录调用工具:'dotnet tool run dotnet-cake''dotnet dotnet-cake'工具'cake.tool'(版本'0.35.0')已成功安装。条目已添加到清单文件C:\repos\test\.config\dotnet-tools.json。

这通过将cake.tool引用添加到tools部分来更新清单,包括所需的版本(当前最新版本 - 您可以根据需要手动更新版本)以及运行工具所需的命令(dotnet-cake):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "version": 1,
  "isRoot": true,
  "tools": {
    "cake.tool": {
      "version": "5.0.0",
      "commands": [
        "dotnet-cake"
      ]
    }
  }
}

当同事克隆存储库并想要运行Cake工具时,他们可以运行以下命令首先恢复工具,然后运行它:

1
2
3
4
5
6
7
8
9
# 恢复清单中指定的NuGet包
dotnet tool restore

# 使用以下形式之一运行工具:
dotnet tool run dotnet-cake
# 或者您可以使用:
dotnet dotnet-cake
# 或者更短:
dotnet cake

或者,从.NET 10预览版6开始,您可以使用更简单的dnxdotnet dnx工具来一次性运行工具。当工具在工具清单中指定时,您可以直接使用dnx,它将自动使用清单中指定的版本:

1
2
3
# 使用以下任一方式一次性运行工具:
dnx Cake.Tool
dotnet dnx Cake.Tool

通过多目标确保兼容性

使用和创建.NET工具有一些稍微令人烦恼的困难。.NET工具只是普通的框架依赖型.NET应用程序,因此它们依赖于目标机器上可用的正确.NET运行时。

作为一个具体示例,如果您构建一个.NET工具,并且它目标为net8.0,那么目标机器上必须安装.NET 8运行时,无论您使用哪个版本的SDK安装该工具。

因此,如果您想支持客户机器上可能安装的任何运行时,那么您需要为多个目标框架构建和打包您的工具。

原则上这很容易做到,因为您可以"仅仅"在项目的.csproj文件的<TargetFrameworks>元素中添加所有需要支持的目标框架:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <!-- 👇 目标所有框架!-->
    <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>

从上面的列表可以看出,如果您真的想支持所有内容,那么需要添加很多目标框架。而且这并非没有缺点。

首先,当您创建这样的多目标应用程序时,您通常将仅限于最低目标框架中存在的API。在上面的示例中,这意味着.NET Core 2.1 API😬

此外,您在此处添加的每个目标框架都会增加NuGet包的大小。当您打包应用程序时,您将为每个目标框架构建它,并将所有内容打包到同一个NuGet包中:

这可以显著增加包的大小,这通常不是问题,但会使所有恢复和dnx操作(例如)变慢。

为您支持的所有目标运行时构建和打包是支持最广泛客户范围的"最安全"方法。但是,如果您愿意承担一点风险,还有一种替代方法。

配置工具前滚

在上一节中,我说在.NET工具中显式定位所有支持的.NET运行时是确保您的工具可以在客户环境中运行的最佳方法,无论他们使用哪个运行时。

然而,一种替代(在许多方面是互补的)方法是不定位所有这些运行时版本。相反,您通过使用<RollForward>元素允许您的应用程序在比其构建时更新的运行时版本上运行。

例如,假设您有一个在.NET 6上工作的工具,并且您不想同时为.NET 7、.NET 8、.NET 9等多目标它。鉴于每个版本的.NET与先前版本具有非常高的兼容性,您可以改为仅为.NET 6构建您的工具,然后告诉dotnet主机允许使用任何可用于.NET 6或更高版本的运行时。

您可以通过在项目文件中设置RollForward=Major来做到这一点:

1
2
3
4
<PropertyGroup>
  <TargetFramework>net6.0</TargetFramework>
  <RollForward>Major</RollForward>
</PropertyGroup>

在项目中设置<RollForward>可确保此属性被复制到与您的工具一起部署的runtimeconfig.json文件中。您可以在NuGet包中找到这个;请注意下面的rollFoward属性反映了您在项目中设置的值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "runtimeOptions": {
    "tfm": "net6.0",
    "rollForward": "Major",
    "framework": {
      "name": "Microsoft.NETCore.App",
      "version": "6.0.0"
    },
    "configProperties": {
      "System.Reflection.Metadata.MetadataUpdater.IsSupported": false
    }
  }
}

为您的工具配置了rollForward后,只要有人能够安装您的.NET工具(只要他们安装了.NET 6+ SDK就可以做到),那么他们将能够运行该应用程序,即使您仅为.NET 6构建和打包了您的应用程序。

请注意,这并不完全安全,因为不能保证.NET运行时在主要版本之间兼容。然而,在实践中它相对安全,并且通常被推荐。

即使您确实为多个目标框架打包,在项目中设置RollForward=Major的最佳原因之一是为了支持未来发布的当前未发布的.NET版本。例如,假设您发布了一个支持.NET 9的.NET工具。默认情况下,当.NET 10发布时,人们将无法运行您的工具,除非您返回并显式添加net10.0目标。通过设置RollForward=Major,您可以确保立即获得一些支持。

方便的dotnet工具技巧

本文的最后一节是在使用.NET工具时可用的一些方便选项的杂项,特别是当您在持续集成(CI)系统中做事时。这些通常是我自己在使用它们时遇到的事情,它们并不总是显而易见的。

使用–source和–tool-path测试本地构建的包

如前所述,.NET工具基本上只是.NET应用程序,因此在大多数情况下,您可以像测试任何其他应用程序一样测试它们。但是,您可能还想显式测试您正在生产的最终工件,即.nupkg文件。

当您测试本地生产的工具时,我建议同时使用--source--tool-path设置:

  • --source 指定从何处安装工具。指向包含.nupkg文件的文件夹以仅从这些包安装,而不是从其他NuGet源安装。
  • --tool-path 指定将工具安装到哪里。.NET工具将安装并解包到此目录,然后可以从此目录运行。

例如:

1
2
3
4
5
6
# 安装位于/app/install/文件夹中的dd-trace包的1.2.3版本
# 并将其安装到/tool路径
dotnet tool install dd-trace \
    --source /app/install/. \
    --tool-path /tool \
    --version 1.2.3

在测试本地构建的包时使用这两个设置可确保您既实际安装了您认为的工具(而不是意外从远程源安装),又不会用这些测试文件"污染"您的本地NuGet缓存。

使用–prerelease安装预发布版本

如果您生产的包具有预发布后缀(即它具有像1.0.0-beta或0.0.1-preview这样的版本,而不是仅仅1.0.0或0.0.1),那么您可能会惊讶地发现您无法轻松地在本地测试它。这是因为在安装预发布版本时必须传递--prerelease标志:

1
2
3
4
5
6
# 安装预发布版本时需要--prerelease标志
dotnet tool install dd-trace \
    --source /app/install/. \
    --tool-path /tool \
    --version 1.2.3-preview \
    --prerelease

请注意,此标志仅从.NET 5+的.NET SDK开始可用。

使用–allow-downgrade提供稳健性

如果您在CI中安装.NET工具,通常应指定版本,以确保您的CI可重复。但是如果工具已经安装了呢?

1
2
> dotnet tool install -g dotnet-serve --version 1.10.175
请求的版本1.10.175低于现有版本1.10.190。

如上面的示例所示,如果您尝试安装低于当前安装版本的工具版本,这将失败。

我认为历史上如果工具已经安装在机器上,您实际上无法安装任何新版本的工具,而是必须使用dotnet tool update,但至少从.NET 9开始,似乎您可以在技术上使用上面的dotnet tool install命令更新到包的新版本。

dotnet tool update命令通过卸载工具然后安装新版本工作,因此您可能认为可以改用那个,但不行:

1
2
> dotnet tool update -g dotnet-serve --version 1.10.175
请求的版本1.10.175低于现有版本1.10.190。

您得到完全相同的错误消息。这里的关键是您需要在运行dotnet tool update时包含--allow-downgrade选项:

1
2
> dotnet tool update -g dotnet-serve --version 1.10.175 --allow-downgrade
工具'dotnet-serve'已成功从版本'1.10.190'更新到版本'1.10.175'

请注意,dotnet tool install --allow-downgrade也有效。似乎这两个命令现在做完全相同的事情,所以我不知道为什么update还没有被弃用,老实说😅

总结

在这篇文章中,我讨论了如何使用.NET工具。我描述了如何使用工具清单安装本地工具,以及在创建工具时的一些考虑因素。特别是,我讨论了关于多目标以确保与客户环境的最大兼容性以及使用RollForward=Major以确保未来兼容性的考虑因素。最后,我提供了一些关于使用.NET工具的一般技巧,特别是当您在CI中构建和测试.NET工具时。

在下一篇文章中,我们将看看.NET 10中即将推出的一些新功能!

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计