一般我们构建传统的NuGet包,都是打包和分发dll程序集文件。
至于打包和分发C#源代码文件的做法,比较少见。
那么这种打包源代码文件的做法,有什么优点和缺点呢?
优点:
- 方便阅读源代码。
- 方便断点调试。
- 减少 Assembly 程序集模块加载个数。
- 更利于发布期间的剪裁(PublishTrimmed 选项)。
- 更利于混淆和保护代码(Internal 级别的源代码)。
缺点:
- 容易外泄原始的源代码文件。
- 随着引入源代码组件越多,越容易引发命名空间和类型名称重复冲突。
经验:
- 不建议也不推荐分发 public 级别的源代码。
- 尽可能严格规范命名类型名称。
- 向目标项目写入源代码组件 version 和 git commit sha-1,方便出问题时排查版本问题。
- 每次改动源代码文件时,尽可能做到向下兼容。
正文:
接下来,我们一起看看如何制作仅打包C#源代码文件,不打包dll程序集文件的C#源代码组件NuGet包。
首先是创建 AllenCai.BuildingBlocks 项目,目录结构如下:
.
├── build
└── src
├── AllenCai.BuildingBlocks
│ ├── AllenCai.BuildingBlocks.csproj
│ ├── Properties
│ │ ├── PackageInfo.cs
│ ├── Assets
│ │ ├── build
│ │ │ └── AllenCai.BuildingBlocks.targets
│ │ └── buildMultiTargeting
│ │ └── AllenCai.BuildingBlocks.targets
│ ├── Collections
│ │ ├── ArrayBuilder.cs
│ │ ├── other...
│ ├── Functional
│ │ ├── Result.cs
│ │ ├── other...
│ ├── ObjectPooling
│ │ ├── DictionaryPool.cs
│ │ ├── other...
│ ├── Text
│ │ ├── StringBuffer.cs
│ │ ├── other...
│ ├── Threading
│ │ ├── ValueTaskEx.cs
│ │ ├── other...
│ ├── bin
│ │ ├── Release
│ │ │ └── other...
│ │ ├── Debug
│ │ │ └── other...
│ └── obj
│ │ ├── other...
│ ├── icon.png
│ ├── other...
├── AllenCai.BuildingBlocks.sln
└── Directory.Build.targets
├── .gitattributes
├── .gitignore
├── README.md
其中 Directory.Build.targets 文件,用来生成描述源代码组件包版本信息的C#源代码文件,输出文件路径为:Properties\PackageInfo.cs。
之所以输出到 Properties 目录,是因为 PackageInfo.cs 的作用其实和以前 .NET Framework 时代每个项目都会包含的 AssemblyInfo.cs 相同。
那么,为什么需要生成这个 PackageInfo.cs 文件呢?
- 因为不再是编译和发布dll,而是直接打包和提供源代码文件,原本被内嵌到dll程序集的版本信息是丢失的。
- 懒,也不希望每次手工维护写入 Version 和
git commit sha-1。
Directory.Build.targets 文件代码如下所示:
<Project>
<Target Name="GeneratePackageInfoToFile" BeforeTargets="PreBuildEvent" Condition="'$(Configuration)' == 'Release'">
<PropertyGroup>
<SharedPackageInfoFile>$(ProjectDir)Properties\PackageInfo.csSharedPackageInfoFile>
PropertyGroup>
<ItemGroup>
<AssemblyAttributes Include="AssemblyMetadata">
<_Parameter1>PackageVersion_Parameter1>
<_Parameter2>$(Version)_Parameter2>
AssemblyAttributes>
<AssemblyAttributes Include="AssemblyMetadata">
<_Parameter1>PackageBuildDate_Parameter1>
<_Parameter2>$([System.DateTime]::Now.ToString("yyyy-MM-dd HH:mm:ss"))_Parameter2>
AssemblyAttributes>
<AssemblyAttributes Include="AssemblyMetadata" Condition="'$(SourceRevisionId)' != ''">
<_Parameter1>PackageSourceRevisionId_Parameter1>
<_Parameter2>$(SourceRevisionId)_Parameter2>
AssemblyAttributes>
ItemGroup>
<MakeDir Directories="$(ProjectDir)Properties"/>
<WriteCodeFragment Language="C#" OutputFile="$(SharedPackageInfoFile)" AssemblyAttributes="@(AssemblyAttributes)" />
<Message Importance="high" Text="SharedPackageInfoFile --> $(SharedPackageInfoFile)" />
<ItemGroup>
<Compile Include="$(SharedPackageInfoFile)" Pack="true" BuildAction="Compile" />
ItemGroup>
Target>
Project>
而 AllenCai.BuildingBlocks.targets 文件,将会被打包到NuGet包。
当这个包被添加引用到目标项目中,MsBuild 将会自动调用它,执行一系列由你定义的动作。
那么,又为什么需要这个 AllenCai.BuildingBlocks.targets 文件呢?
它其实是非必须的,根据项目实际情况而定,没有这个 targets 文件也是可以的。
但这样的话,可能引用这个源代码组件包的开发者会在刚引入时遇到一系列问题,导致这个源代码组件包对开发者不友好。
比如源代码文件中使用了不安全代码,而目标项目的属性值是 false,那么目标项目在编译时就会报错。
因此需要这个 targets 文件来检查和自动设置为 true。
如以下示例代码(build\AllenCai.BuildingBlocks.targets):
<Project>
<Target Name="UpdateLangVersionAndAllowUnsafeBlocks" BeforeTargets="BeforeCompile">
<PropertyGroup>
<OldAllowUnsafeBlocks>$(AllowUnsafeBlocks)OldAllowUnsafeBlocks>
<AllowUnsafeBlocks Condition=" '$(AllowUnsafeBlocks)' == '' or $([System.String]::Equals('$(AllowUnsafeBlocks)','false','StringComparison.InvariantCultureIgnoreCase')) ">TrueAllowUnsafeBlocks>
PropertyGroup>
<Message Importance="high" Condition=" '$(AllowUnsafeBlocks)' != '$(OldAllowUnsafeBlocks)' " Text="Update AllowUnsafeBlocks to $(AllowUnsafeBlocks)" />
Target>
Project>
以及 buildMultiTargeting\AllenCai.BuildingBlocks.targets 文件代码如下所示:
<Project>
<Import Project="..\build\AllenCai.BuildingBlocks.targets" />
Project>
需要注意的是,这个 targets 文件需要与 ProjectName 或 PackageId 保持一致。
最后 AllenCai.BuildingBlocks.csproj 文件代码如下所示:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net5.0;net6.0;net7.0;net8.0TargetFrameworks>
<LangVersion>defaultLangVersion>
<Nullable>enableNullable>
<AllowUnsafeBlocks>trueAllowUnsafeBlocks>
<ImplicitUsings>disableImplicitUsings>
<ProduceReferenceAssembly>falseProduceReferenceAssembly>
<GenerateDocumentationFile>falseGenerateDocumentationFile>
<Version>0.0.1Version>
PropertyGroup>
<PropertyGroup>
<Title>AllenCai BuildingBlocksTitle>
<Description>提供一组最常用的通用软件模块,以文件链接的方式被包含到引用项目中。Description>
<Authors>Allen.CaiAuthors>
<Copyright>Copyright © Allen.Cai 2015-$([System.DateTime]::Now.Year) All Rights ReservedCopyright>
<ContentTargetFolders>contentFiles\cs\any\AllenCai.BuildingBlocks;content\cs\any\AllenCai.BuildingBlocksContentTargetFolders>
<DevelopmentDependency>trueDevelopmentDependency>
<IncludeBuildOutput>falseIncludeBuildOutput>
<NoPackageAnalysis>trueNoPackageAnalysis>
<PackageProjectUrl>http://192.168.1.88:5555/allen/allencai.buildingblocks/PackageProjectUrl>
<PackageReadmeFile>README.mdPackageReadmeFile>
<RepositoryUrl>http://192.168.1.88:5555/allen/allencai.buildingblocks.gitRepositoryUrl>
<RepositoryType>gitRepositoryType>
<PackageIcon>icon.pngPackageIcon>
PropertyGroup>
<ItemGroup>
<None Include="icon.png" Pack="true" PackagePath="\" />
<None Include="..\..\README.md" Link="README.md" Pack="true" PackagePath="\" />
<Content Include="**\*.cs" Exclude="obj\**\*.cs" Pack="true" BuildAction="Compile" />
ItemGroup>
Project>
其中三个属性比较重要,DevelopmentDependency 和 IncludeBuildOutput 以及 ContentTargetFolders。
- 将
DevelopmentDependency设置为true,表示这个 NuGet 包仅在开发期间依赖。 - 将
IncludeBuildOutput设置为false,表示打包时不包含编译输出的 dll 文件。 - 重写
ContentTargetFolders,将会改变这些源代码文件在目标项目中的虚拟文件系统布局。
如有不明白,欢迎留言,互相探讨。
截止本文,我刚搜到有 MVP大佬-吕毅 也写了类似教程,大家也可以参考:从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目) - walterlv