Skip to content

dotnet 根据基线包版本实现库版本兼容

Updated: at 08:22,Created: at 00:31

本文来告诉大家如何根据 基线包版本 的功能来实现自动在构建过程中,告诉开发者,当前版本是否存在不兼容旧版本的变更。其不兼容变更包括二进制中断变更和 API 不兼容变更和源代码中断变更。可以让库开发者花更少的精力在测试兼容性上

今天看到了队长推送的 .NET 6新特性试用 Nuget包验证 博客,才回忆起此功能。这个功能是给库和框架开发者使用的,用于处理多版本兼容性问题

背景

只有对一个库或框架准备对外发布且长期维护,以及期望给其他开发者使用时,才需要考虑库或框架的兼容性问题。越是开发底层的库,兼容性问题就越加重要。此重要性,只有自己参与开发,踩够坑之后,才能有所体会

换句话说,判断一位开发者是不是库或框架的老司机开发者,可以通过他的兼容性处理上来看出。哈哈,需要说明的是,不是所有老司机开发者都是库或框架开发方向的,这是判断有经验的开发者的充分不必要条件

开始之前,先聊聊什么是兼容性问题。兼容性可以分为以下不兼容变更:

此外,还有更换了底层运行时框架的变更等,但这些就不在本文讨论范围了

更多请参阅官方文档的详细描述: 重大更改和 .NET 库 Microsoft Docs

对于使用库或框架的开发者来说,一方面又期望用上新版本的强大功能,另一方面又怕有不兼容的变更,需要花费大量的精力在更新上面。如果库或框架的开发者,可以保持好兼容性,那么升级版本是一个很轻松的事情

对于咱 dotnet 系的大部分库或框架开发者来说,在开发过程中,考虑兼容性是一个必备的选项。那如果真的需要变更 API 了呢?问题也不大,别忘了咱还有版本号规则

版本号规则

基本所有 dotnet 系上,正经的库和框架都会遵循约定的版本号规则,从而让开发者在使用任何库的时候,通过版本号都能明确其中的含义,决定自己是否应该升级到最新版本

无异议的版本规则是,版本号由四个部分组成,分为 主版本号.次版本号.构建号.修订号 四个部分。其中的 构建号 和 修订号 都可忽略不写。各个部分的含义如下

此外,有一些库毕竟激进,需要发布预览版本等,可以考虑采用语义版本号的方法,请看 语义版本号(Semantic Versioning) - walterlv - 博客园

通过如上的说明,可以了解到,如果不想刷主版本号,那就要求库或框架保持兼容旧版本。兼容旧版本需要在开发时,投入精力了解是否存在不兼容的更改,然而纯依靠手动去阅读代码了解是否存在不兼容的变更,当然是不靠谱的。本文将告诉大家如何使用 EnablePackageValidationPackageValidationBaselineVersion 功能,自动让构建工具告诉开发者当前的更改是否存在不兼容的更改,从而更好保持库或框架的兼容

使用方法

一如既往的简单,只需要在项目文件上,添加如下代码即可

<EnablePackageValidation>true</EnablePackageValidation>
<PackageValidationBaselineVersion>基于的版本号</PackageValidationBaselineVersion>

例如当前是 2.0.0 的版本,期望进行对 1.0.0 包版本的兼容性测试,可以将 PackageValidationBaselineVersion 的值更改为 1.0.0 版本,如下面代码

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<PackageVersion>2.0.0</PackageVersion>
<EnablePackageValidation>true</EnablePackageValidation>
<PackageValidationBaselineVersion>1.0.0</PackageValidationBaselineVersion>
</PropertyGroup>

如此,在存在中断性(也就是不兼容,需要代码适配)变更时,在会在构建时给出提示,同时让构建不通过

例子

如何更好的使用此功能,还请让我用一个例子来告诉大家。此例子完全从 官方文档 抄的

在第一个版本时,作为 1.0.2 的版本的 NuGet 包,已对外发布。在进行 1.1.0 版本开发时,期望能做到完全的兼容第一个版本。利用 PackageValidationBaselineVersion 的功能,在 csproj 项目文件上,加上如下代码

<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<PackageVersion>1.1.0</PackageVersion>
<EnablePackageValidation>true</EnablePackageValidation>
<PackageValidationBaselineVersion>1.0.2</PackageValidationBaselineVersion>
</PropertyGroup>
</Project>

通过在 PackageValidationBaselineVersion 执行定了基线包版本为 1.0.2 即可采用此指定的版本进行基线包版本对比。例如几周后,你的任务是为库添加对连接超时的支持,代码的 Connect 方法目前如下所示:

public static HttpClient Connect(string url)
{
// ...
}

由于连接超时是一个高级配置设置,因此你认为可以添加一个可选参数,更改如下:

public static HttpClient Connect(string url, TimeSpan timeout = default)
{
// ...
}

更改之后,构建过程可以正常,但是在打包的时候,将会收到如下提示,打包失败

D:\demo>dotnet pack
Microsoft (R) Build Engine version 17.0.0-preview-21460-01+8f208e609 for .NET
Copyright (C) Microsoft Corporation. All rights reserved.
Determining projects to restore...
All projects are up-to-date for restore.
You are using a preview version of .NET. See: https://aka.ms/dotnet-core-preview
PackageValidationThrough -> D:\demo\bin\Debug\net6.0\PackageValidationThrough.dll
Successfully created package 'D:\demo\bin\Debug\PackageValidationThrough.2.0.0.nupkg'.
C:\Program Files\dotnet\sdk\6.0.100-rc.1.21463.6\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Compatibility.Common.targets(32,5): error CP0002: Member 'A.B.Connect(string)' exists on [Baseline] lib/net6.0/PackageValidationThrough.dll but not on lib/net6.0/PackageValidationThrough.dll [D:\demo\PackageValidationThrough.csproj]

或者中文版本的提示如下

用于 .NET 的 Microsoft (R) 生成引擎版本 17.0.0-preview-21501-01+bbcce1dff
版权所有(C) Microsoft Corporation。保留所有权利。
正在确定要还原的项目…
所有项目均是最新的,无法还原。
你正在使用 .NET 的预览版。请查看 https://aka.ms/dotnet-core-preview
NallcearreyiHernareferkear -> C:\lindexi\NallcearreyiHernareferkear\NallcearreyiHernareferkear\bin\Debug\net6.0\NallcearreyiHernareferkear.dll
已成功创建包“C:\lindexi\NallcearreyiHernareferkear\NallcearreyiHernareferkear\bin\Debug\NallcearreyiHernareferkear.2.0.0.nupkg”。
C:\Program Files\dotnet\sdk\6.0.100-rc.2.21505.57\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Compatibility.Common.targets(32,5): error CP0002: Member 'NallcearreyiHernareferkear.Foo.Connect(string)' exists on [Baseline] lib/net6.0/NallcearreyiHernareferkear.dll but not on lib/net6.0/NallcearreyiHernareferkear.dll [C:\lindexi\NallcearreyiHernareferkear\NallcearreyiHernareferkear\NallcearreyiHernareferkear.csproj]

如此通过打包失败,提示的 CP0002 失败,可以了解到,自己没有做到让当前版本对写入到 PackageValidationBaselineVersion 的兼容。此时要做的事情,要么是废弃掉对 PackageValidationBaselineVersion 的兼容,也就是删除此属性,同时升级主版本号,告诉其他开发者,当前版本存在不兼容。要么是更改 API 定义,更改到兼容

例如以上的代码,虽然加上了一个默认参数,可以实现到源代码兼容。但是大家都知道,这是二进制不兼容的,如果直接替换 DLL 文件,而不经过编译,将会在运行的过程中,因为找不到对应的方法而失败

什么情况下会遇到没有重新构建,只是替换 DLL 文件而已?在于是其他底层库的依赖引用,例如再有另一个库 C 也引用了此,而库 C 打出的 NuGet 包被最终项目所引用。当最终项目升级版本时,由于 Connect 方法被更改,从而让库 C 里面的对应逻辑找不到方法,而在运行时失败

因此为了做到这部分的兼容,可以考虑作为重载的方法更改,更改如下

public static HttpClient Connect(string url)
{
return Connect(url, Timeout.InfiniteTimeSpan);
}
public static HttpClient Connect(string url, TimeSpan timeout)
{
// ...
}

这样进行重新打包,即可看到打包成功,兼容 PackageValidationBaselineVersion 的 1.0.2 版本

原理

此功能是依托于 NuGet 包发布而拿到指定版本号规则的,和 使用基于 Roslyn 的 Microsoft.CodeAnalysis.PublicApiAnalyzers 来追踪项目的 API 改动,帮助保持库的 API 兼容性 - walterlv 的方法是完全不相同的

本文介绍的方法,是在 PackageValidationBaselineVersion 里面,声明的包版本,在构建过程中,通过 NuGet 去拉取对应的版本,接着通过 DLL 导出类型的对比,从而了解是否存在不兼容的变更

也就是说在 PackageValidationBaselineVersion 里面写入的版本号,要求是可以在 NuGet 源里面(无论是 nuget.org 源,还是你的私有的源,还是你的本机文件夹都可以)拉到对应的版本。由此版本里面的 DLL 执行具体的对比逻辑。这也就要求了此功能只能用在简单的 NuGet 上,对于很多上了黑科技的 NuGet 包是无法执行的。例如使用 SourceYard 打包的源代码包

本文介绍的方法,对比使用基于 Roslyn 的 Microsoft.CodeAnalysis.PublicApiAnalyzers 来追踪项目的 API 改动,帮助保持库的 API 兼容性 的方法来说,优势在于不需要带上 PublicAPI.Unshipped.txtPublicAPI.Shipped.txt 文件,此两个文件夹特别好在团队开发时进行冲突,而且需要进行手动管理。但是缺点在于本文介绍的方法功能单一,也依赖 NuGet 包版本

代码

本文以上的代码放在githubgitee 欢迎访问

可以通过如下方式获取本文的源代码,先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 95692dcaabfb0d143dffa8e31c0a1ad00e7c2e74

以上使用的是 gitee 的源,如果 gitee 不能访问,请替换为 github 的源

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git

获取代码之后,进入 NallcearreyiHernareferkear 文件夹

更多阅读

听龙华讲公共组件 CBB 建设笔记

创建CBB心得

dotnet CBB 为什么决定推送 Tag 才能打包

开源公共组件仓库的更新日志应该如何写

dotnet 使用 Obsolete 特性标记成员过时保持库和框架的兼容性

语义版本号(Semantic Versioning) - walterlv - 博客园

使用基于 Roslyn 的 Microsoft.CodeAnalysis.PublicApiAnalyzers 来追踪项目的 API 改动,帮助保持库的 API 兼容性 - walterlv

重大更改和 .NET 库 Microsoft Docs

Assembly versioning Microsoft Docs


知识共享许可协议

原文链接: http://blog.lindexi.com/post/dotnet-%E6%A0%B9%E6%8D%AE%E5%9F%BA%E7%BA%BF%E5%8C%85%E7%89%88%E6%9C%AC%E5%AE%9E%E7%8E%B0%E5%BA%93%E7%89%88%E6%9C%AC%E5%85%BC%E5%AE%B9

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。 欢迎转载、使用、重新发布,但务必保留文章署名 林德熙 (包含链接: https://blog.lindexi.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我 联系