包装验证的重要性解释和入门教程

109 阅读6分钟

在这篇博文中,我将展示新的软件包验证工具,该工具将在.NET 6中可用。它可以确保你的软件包消费者在所有的.NET平台和版本中都有很好的体验,而且你没有意外地对你的软件包的前一版本进行任何破坏性的修改。如果你对此感兴趣,请继续阅读!

为什么验证很重要

有了.NET Core和Xamarin,我们已经使跨平台成为库作者的主流要求。然而,我们缺乏对跨目标包的验证工具,这可能导致包不能很好地工作,这反过来又会伤害我们的生态系统。这对于新兴的平台来说尤其成问题,因为这些平台的采用率还不够高,不值得图书馆作者特别关注。

作为SDK的一部分,我们提供的工具对多目标包的格式化验证接近于零。例如,一个针对.NET 6.0和.NET标准2.0的多目标包需要确保针对.NET标准2.0二进制编译的代码能够在.NET 6.0二进制中运行。我们已经在野外看到了这个问题,甚至在第一方,例如Azure AD库。

我们很容易认为,如果消耗该变化的源码能够继续编译而不发生变化,那么该变化就是安全和兼容的。然而,某些变化可能在C#中运行良好,但如果消费者没有重新编译,就会在运行时引起问题,例如,增加一个默认参数或改变一个常量的值。

包验证工具将允许库开发人员验证他们的包是一致的和格式良好的。它包括验证在不同的版本中是否有破坏性的变化。它将验证包对于所有不同的运行时间特定的实现都有相同的公共API集。它还会帮助开发者抓住任何适用性漏洞。

如何在你的项目中加入包验证功能

包验证目前是作为一个MSBuild SDK包被运送的,它可以被一个项目所使用。它是一组任务和目标,在生成包后调用dotnet pack (或者在dotnet build ,如果你把GeneratePackageOnBuild 设为true )时运行。

要引用它,你需要使用新的<Sdk> 语法:

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

  <Sdk Name="Microsoft.DotNet.PackageValidation" Version="1.0.0-preview.5.21302.8" />

  <PropertyGroup>
    <TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
  </PropertyGroup>

</Project>

在接下来的章节中,我将带领你完成一组场景,展示你如何验证你自己的包。

验证兼容的框架

包含兼容框架的包需要确保针对一个框架编译的代码可以在另一个框架上运行。兼容框架对的例子有:

  • .NET标准2.0和.NET 6.0
  • .NET 5.0和.NET 6.0

在这两种情况下,你的消费者可以根据.NET标准2.0或.NET 5.0构建,并在.NET 6.0上运行。如果你的二进制文件在这些框架之间不兼容,消费者可能会出现编译和/或运行时错误。

包验证将在打包时捕获这些错误。下面是一个场景的例子。

假设你在写一个游戏,其中有大量的字符串操作。你需要同时支持.NET Framework和.NET Core消费者。你开始只是针对.NET标准2.0,但现在你意识到你想利用.NET 6.0的跨度来避免不必要的字符串分配。为了做到这一点,你现在想对.NET标准2.0和.NET 6.0进行多重定位。

你写了以下代码:

#if NET6_0_OR_GREATER
    public void DoStringManipulation(ReadOnlySpan<char> input)
    {
        // use spans to do string operations.
    }
#else
    public void DoStringManipulation(string input)
    {
        // Do some string operations.
    }
#endif

然后你尝试打包项目(使用dotnet pack cmd或使用VS),但失败了,出现了以下错误:

CompatibleFrameworks, Package Validation

你明白你不应该排除DoStringManipulation(string) ,而只是为.NET 6.0提供一个额外的DoStringManipulation(ReadOnlySpan<char>) 方法,并相应地修改代码:

#if NET6_0_OR_GREATER
    public void DoStringManipulation(ReadOnlySpan<char> input)
    {
        // use spans to do string operations.
    }
#endif
    public void DoStringManipulation(string input)
    {
        // Do some string operations.
    }

你尝试再次打包该项目:

CompatibleFrameworksSuccessful, Package Validation

针对基线包版本的验证

包验证也可以帮助你根据之前发布的稳定版本的包来验证你的库项目。为了使用这个功能,你需要在你的项目中添加PackageValidationBaselineVersionPackageValidationBaselinePath

软件包验证将检测任何已发布的目标框架上的任何破坏性变化,也将检测是否有任何目标框架支持被放弃。

例如,考虑以下情况:你正在处理AdventureWorks.Client NuGet包。你想确保你不会意外地做出破坏性的改变,所以你配置了你的项目,指示包的验证工具对包的上一个版本运行API兼容性。

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

  <Sdk Name="Microsoft.DotNet.PackageValidation" Version="1.0.0-preview.5.21302.8" />

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <PackageVersion>2.0.0</PackageVersion>
    <PackageValidationBaselineVersion>1.0.0</PackageValidationBaselineVersion>
  </PropertyGroup>

</Project>

几周后,你的任务是为你的库增加对连接超时的支持。连接方法目前是这样的:

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

由于连接超时是一个高级的配置设置,你认为你可以只添加一个可选参数:

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

然而,当你尝试打包时,它抛出了一个错误:

BaselineVersion

你意识到,虽然这不是一个源码的破坏性改变,但却是一个二进制的破坏性改变。你通过添加一个重载来解决这个问题:

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

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

你试着再次打包该项目:

BaselineVersionSuccessful

针对不同运行时的验证

你可能会选择在你的nuget包中为不同的运行时间提供不同的实现程序集。在这种情况下,你需要确保这些程序集与编译时的程序集兼容。

例如,考虑以下情况:你正在做一个库,涉及到一些分别对Unix和Windows API的互操作调用。你写了下面的代码:

#if Unix
    public static void Open(string path, bool securityDescriptor)
    {
        // call unix specific stuff
    }
#else
    public static void Open(string path)
    {
        // call windows specific stuff
    }
#endif

由此产生的包结构看起来像:

lib/net6.0/A.dll 
runtimes/unix/lib/net6.0/A.dll

lib/net6.0/A.dll 无论底层操作系统如何,在编译时都会使用 ,对于非Unix系统在运行时也会使用,但对于Unix系统在运行时则会使用 。lib/net6.0/A.dll runtimes/unix/lib/net6.0/A.dll

当你试图打包这个项目时,你得到一个错误:

MultipleRuntimes, Package Validation

你很快意识到你的错误,并将A.B.Open(string) 也添加到unix运行时:

#if Unix
    public static void Open(string path, bool securityDescriptor)
    {
        // call unix specific stuff
    }

    public static void Open(string path)
    {
        // throw not supported exception
    }
#else
    public static void Open(string path)
    {
        // call windows specific stuff
    }
#endif

你再次尝试打包该项目。

MultipleRuntimesSuccessful, Package Validation

路线图

我们将继续通过每月的更新来增加更多的功能,直到我们在今年晚些时候发布一个稳定版本。一些已经在进行中的功能是错误抑制和nullability注释的兼容规则。

分享您的反馈

我们对这个版本感到兴奋,并期待着你的反馈。让我们知道你对该产品的看法。

让我们知道你的想法!