语义化版本控制(简称SemVer)是一种软件版本控制方案,规定版本号由三部分组成,形式为<主版本>.<次版本>.<修订版本>,例如1.0.2,并且可以选择性地添加预发布后缀,形式为-<预发布版本>,如1.0.2-beta

SemVer也许是当今最广泛使用的版本控制方案。例如,Nugetnpm都推荐并支持它,VS Code也使用它

在大多数使用GitHub Releases功能发布版本的GitHub仓库中,你会在主页的最新发布徽章上看到一个SemVer版本号,如下面的截图所示:

我在构建ASP.NET Core API项目时经常需要设置一个SemVer版本号,然后在运行时读取或报告这个版本号。

例如,如果我构建了一个版本设置为1.0.2-beta的最小API,这将会通过API暴露的/version端点报告,如下面来自Hoppscotch的截图所示(这是一个类似于Postman的工具,其便利之处在于它可以在浏览器中运行):

检查从部署的服务(如Web应用程序和API)报告的版本是否正确,是我CD管道的关键部分,也是我用来确定部署是否成功的一些冒烟测试之一。

在.NET程序集上设置SemVer版本号时的一个微小复杂之处在于,.NET最初使用四个部分的版本号,如1.0.3.212,而程序集仍然具有这些(程序集是.NET术语,指的是编译为.NET字节码的代码单元,其中最典型的是dll和exe文件)。

另一个问题是.NET在同一个程序集中不只有一个而是有多个略有不同的版本号。

在本文中,我将向您展示如何在构建过程中将这些特点规避,并将SemVer版本号打在.NET程序集上。也就是说,在编译后的.exe.dll上,以及如何在运行时读取它。

目录

SEMBer版本号的构成

考虑一个SEMBer版本号,如1.0.21.0.2-beta。它具有<主版本号>.<次版本号>.<修订号><预发布版本>的形式

以下是各个组件的含义:

版本号中的<主版本>部分只有在新的发布版本会破坏现有(最新)版本时才会增加。

在UI应用程序的情况下,客户端可能指的是人类用户。所以,如果新版本会破坏用户现有的资产,如工作流定义,这就需要增加主版本号。在这种情况下,如果之前的版本是1.0.2,新版本应该是2.0.0(版本号中的所有较低部分都会重置)。

在库的情况下,例如Nuget或NPM上的库包,客户端将是其他代码。所以,如果新版本会破坏现有客户端代码,即它与其自己的前一个版本不向后兼容,那么<主版本>部分也会增加。

<次版本>会在添加了新功能但新版本仍然向后兼容时增加。所以从1.0.2会变为1.1.0

<修订版本>会在即使没有破坏性变化且未添加新功能时也需要发布新版本时增加。例如,如果有必须发布的bug修复,就可能发生这种情况。

-<预发布>后缀是可选的。当软件需要在预发布测试阶段(如alpha和beta)提供时,通常会附加到三部分版本号后面。例如,在普遍发布您的软件版本1.0.2之前,您可以将其作为1.0.2-beta提供给您的测试用户。

组件可以是您选择的任何字符串,唯一的要求是它要么是一个字母数字标识符,如beta12alpha2(只能包含字母或数字),或者是多个字母数字标识符,由点(.)分隔,例如development.version

一个.NET程序集的许多版本号

正如Andrew Lock的关于.NET版本化的文章所解释的,一个.NET程序集不只有一个,而是有几个不同的版本号:

  • 程序集版本号(AssemblyVersion):这是一个四部分组成的版本号,例如1.0.2.0。它被运行时用来加载链接的程序集。

  • 文件版本号(FileVersion):这是在Windows文件资源管理器中,当您右键点击程序集并选择“属性”时,报告的.dll文件的版本号。

  • 信息版本: 又一个版本号,与FileVersion一样,可以在Windows中右键点击程序集并选择属性对话框中看到。它可以包含字符串,而不仅限于AssemblyVersion和FileVersion受限制的整数和点。

  • 包版本: 如果项目是一个Nuget包,这将是程序集所属的包的版本号。

所有这些版本号在编译时作为元数据嵌入到程序集中。您可以使用JetBrains dotPeek(免费)或Red gate Reflector(非免费)或类似的工具检查程序集时看到它们。

在Windows资源管理器中右键点击程序集文件并选择属性时出现的属性对话框的详细信息选项卡中,也可以看到FileVersion和信息版本。

在上述截图中,“产品版本”是“InformationalVersion”的标题,而“文件版本”是“FileVersion”的标题。

在上述描述的四种版本号类型中,只有前三种适用于任何程序集(即程序集是否为Nuget包的一部分)。

在这三种中,如果尝试设置只有三个数字(加上可选的预发布后缀)的SemVer版本,AssemblyVersion总是在第四位添加一个0。例如,如果在构建过程中尝试设置SemVer版本为1.0.2-beta,然后在运行时在程序集中读取AssemblyVersion值,它将是1.0.2.0

FileVersion也是这样,如上述截图中所示。

信息版本(InformationalVersion)是唯一一个在构建时设置为与服务器版本完全相同的版本号,如上述截图所示。

因此,信息版本(InformationalVersion)是运行时用于检索程序集SemVer版本的那个版本。

如何设置SemVer版本号

在构建过程中为程序集设置SemVer版本号需要做两件事。

首先,在项目csproj文件的<PropertyGroup>元素中,添加元素<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>

<PropertyGroup>
 ...
 <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion> 
</PropertyGroup>

该问题所述,这确保了InformationalVersion被设置为与我们指定的SemVer版本号完全一致,并且末尾不会附加+<哈希码>

其次,将版本号作为传递给dotnet build命令的Version属性的值,例如:

dotnet build --configuration Release -p Version=1.0.2-beta

这将设置编译后的程序集(.exe或.dll文件)中的InformationalVersion为1.0.2-beta

顺便说一下,它还会设置AssemblyVersion和FileVersion(将在1.0.2的末尾添加一个额外的0),但我们不关心这些。

请注意,您可以在csproj文件中的<PropertyGroup>元素中设置MS Build属性<Version>1.0.2-beta</Version>,而不是在命令行上传递Version参数。然而,向dotnet build传递Version参数的值更简单,因为每次版本号增加时都不需要修改csproj文件。这在CD管道中很有用。此外,默认情况下,csproj文件没有与版本控制相关的任何属性。

如何在运行时读取程序集的SemVer版本

读取运行时InfromationalVersion的代码如下:

string? version = Assembly.GetEntryAssembly()?.
  GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.
  InformationalVersion;

在我的最小API中,为了添加一个/version端点,就像我在上面的引言部分展示的那样,我将上面的代码片段放入Program.cs中,然后立即在后面添加以下代码片段。注意,整个东西应该出现在调用builder.Build() 之前:

//这个匿名类型的对象将被
//序列化为响应体中的JSON
//当由处理程序返回时
var objVersion = new { Version = version ?? "" };

//其他代码
//var app = builder.Build()

在调用builder.Build()之后,我创建/version端点的处理程序:

app.MapGet("/version", () => objVersion);

现在当我运行API项目并调用/version端点时,我会在HTTP响应体中收到版本号的JSON对象:

{
  "version": "1.0.2-beta"
}

这就是引言中Hoppscotch截图所展示的。

结论

本文向您展示了如何在您的.NET程序集、库或应用程序中设置一个SemVer版本号。

它还向您展示了如何在运行时读取版本号。