• 如何使用csproj构建C#源代码组件NuGet包?


    一般我们构建传统的NuGet包,都是打包和分发dll程序集文件。

    至于打包和分发C#源代码文件的做法,比较少见。

    那么这种打包源代码文件的做法,有什么优点和缺点呢?

    优点:

    1. 方便阅读源代码。
    2. 方便断点调试。
    3. 减少 Assembly 程序集模块加载个数。
    4. 更利于发布期间的剪裁(PublishTrimmed 选项)。
    5. 更利于混淆和保护代码(Internal 级别的源代码)。

    缺点:

    1. 容易外泄原始的源代码文件。
    2. 随着引入源代码组件越多,越容易引发命名空间和类型名称重复冲突。

    经验:

    1. 不建议也不推荐分发 public 级别的源代码。
    2. 尽可能严格规范命名类型名称。
    3. 向目标项目写入源代码组件 version 和 git commit sha-1,方便出问题时排查版本问题。
    4. 每次改动源代码文件时,尽可能做到向下兼容。

    正文:

    接下来,我们一起看看如何制作仅打包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 文件呢?

    1. 因为不再是编译和发布dll,而是直接打包和提供源代码文件,原本被内嵌到dll程序集的版本信息是丢失的。
    2. 懒,也不希望每次手工维护写入 Versiongit 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>
    

    其中三个属性比较重要,DevelopmentDependencyIncludeBuildOutput 以及 ContentTargetFolders

    1. DevelopmentDependency 设置为 true,表示这个 NuGet 包仅在开发期间依赖​。
    2. IncludeBuildOutput 设置为 false,表示打包时不包含编译输出的 dll​ 文件。
    3. 重写 ContentTargetFolders,将会改变这些源代码文件在目标项目中的虚拟​文件系统布局。

    如有不明白,欢迎留言,互相探讨。

    截止本文,我刚搜到有 MVP大佬-吕毅 也写了类似教程,​大家也可以参考:从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目) - walterlv

  • 相关阅读:
    MyBatis 的注解实现方法整合(完成数据的增删改查操作)
    【web-5】HTTP/HTTPS
    【Linux】《Linux命令行与shell脚本编程大全 (第4版) 》笔记-Chapter19-初识 sed 和 gawk
    【算法笔记】单源最短路问题——Dijkstra算法(无优化/优先队列/set优化)
    spaCy库的实体链接踩坑,以及spaCy-entity-linker的knowledge_base下载问题
    jdwp-(ide-proxy)交互数据流1
    Ubuntu/CentOS 磁盘分区扩展
    2022年金砖国家职业技能大赛(决赛)网络空间安全赛项 | 浙江赛区选拔赛 任务书
    Elasticsearch 是什么
    【Docker从入门到入土 3】Docker镜像的创建方法
  • 原文地址:https://www.cnblogs.com/VAllen/p/18255504/how-to-use-csproj-to-build-the-csharp-source-code-component-nuget-package