如何使用csproj构建C#源代码组件NuGet包?
一般我们构建传统的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>
<!--
将代码版本信息输出到C#文件中,使用者在项目中引入本组件源码,能够看到版本信息。
且在使用者项目编译为程序集文件后,也能够保留本组件版本信息。
-->
<Target Name="GeneratePackageInfoToFile" BeforeTargets="PreBuildEvent" Condition="'$(Configuration)' == 'Release'">
<PropertyGroup>
<SharedPackageInfoFile>$(ProjectDir)Properties\PackageInfo.cs</SharedPackageInfoFile>
</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 文件也是可以的。
但这样的话,可能引用这个源代码组件包的开发者会在刚引入时遇到一系列问题,导致这个源代码组件包对开发者不友好。
比如源代码文件中使用了不安全代码,而目标项目的<AllowUnsafeBlocks>
属性值是 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')) ">True</AllowUnsafeBlocks>
</PropertyGroup>
<!--当属性项被修改时,在Build控制台输出提示-->
<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.0</TargetFrameworks>
<LangVersion>default</LangVersion>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ImplicitUsings>disable</ImplicitUsings>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<Version>0.0.1</Version>
</PropertyGroup>
<!--一些与NuGet包相关的属性项-->
<PropertyGroup>
<Title>AllenCai BuildingBlocks</Title>
<Description>提供一组最常用的通用软件模块,以文件链接的方式被包含到引用项目中。</Description>
<Authors>Allen.Cai</Authors>
<Copyright>Copyright Allen.Cai 2015-$([System.DateTime]::Now.Year) All Rights Reserved</Copyright>
<ContentTargetFolders>contentFiles\cs\any\AllenCai.BuildingBlocks;content\cs\any\AllenCai.BuildingBlocks</ContentTargetFolders>
<!--该属性项声明了仅在开发期间依赖,并且不传递其自身的依赖项,这将导致目标项目需要主动引入间接依赖项-->
<DevelopmentDependency>true</DevelopmentDependency>
<!--打包时不包含编译输出的文件-->
<IncludeBuildOutput>false</IncludeBuildOutput>
<!--该属性项仅用于源生成器(SourceGenerator)项目,从 Visual Studio 2022 v16.10及以上版本开始支持-->
<!--<IsRoslynComponent>true</IsRoslynComponent>-->
<!--跳过包分析-->
<NoPackageAnalysis>true</NoPackageAnalysis>
<PackageProjectUrl>http://192.168.1.88:5555/allen/allencai.buildingblocks/</PackageProjectUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>http://192.168.1.88:5555/allen/allencai.buildingblocks.git</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageIcon>icon.png</PackageIcon>
</PropertyGroup>
<ItemGroup>
<!-- <PackageReference Include="System.Reactive" Version="5.0.0" /> -->
<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
如何使用csproj构建C#源代码组件NuGet包?的更多相关文章
- 一个技术汪的开源梦 —— 基于 .Net Core 的组件 Nuget 包制作 & 发布
一个技术汪的开源梦 —— 目录 微软的 ASP.Net Core 强化了 Nuget 的使用,所有的 .Net Core 组件均有 Nuget 管理,所以有必要探讨一下 .Net Core 组件制作 ...
- 使用GUI工具高效构建你自己的Nuget包
写这篇文章的原因是我在学习构建nuget包的时候,发现了一个官方推荐的GUI工具,而官方的工具介绍文章已经过时,一些地方和现在最新版本的工具有些差异,所以特意利用假期最后一个下午写下来,希望能帮助更多 ...
- 记录使用 Cake 进行构建并制作 nuget 包
书接上一回(https://www.cnblogs.com/h82258652/p/4898983.html)?[手动狗头] 前段时间折腾了一下,总算是把我自己的图片缓存控件(https://gith ...
- 容器环境下如何将NuGet包XML文档添加到Swagger
容器环境下将NuGet包XML文档添加到Swagger 在.NET Core项目开发过程中,为了实现代码复用,我们将可以重复使用的部分拆分成一个个小的NuGet包.这些NuGet包可以在其他系统中复用 ...
- 自定义Nuget包的技巧一二
背景: 在项目中, 通常会拆分成核心库(Core)和应用(App)两个部分.核心库由专人维护, 不同的App是不同的团队,但都引用了核心库.当核心库需要升级更新时,有的应用会更新,有的不会--可能是没 ...
- 让你发布的nuget包支持源代码调试
前情概要 在不久的从前(也还是要以年为单位哈), 我们如果需要调试第三方代码, 或者框架代码很麻烦. 需要配置symbols, 匹配原始代码路径等. 为此, MS推出了 Source Link 功能, ...
- [UWP] 为WinRT组件创建Nuget包
Nuget 是 dotnet 开发中必不可少的包管理工具,但不仅仅局限于 dotnet 项目,在 VS 中使用 C++ 开发的时候,也可以使用 Nuget 来引用第三方组件.同样也可以用 Nuget ...
- 在 VS 2013/2015 中禁用 nuget 包的源代码管理
对于加入源代码管理如TFS的解决方案,当使用nuget获取包时,下载的包并没有自动从源代码管理中排除,导致包(packages文件夹)会一同上传到服务器. 若要排除nuget包的源代码管理,须在 解决 ...
- .NET持续集成与自动化部署之路第二篇——使用NuGet.Server搭建公司内部的Nuget(包)管理器
使用NuGet.Server搭建公司内部的Nuget(包)管理器 前言 Nuget是一个.NET平台下的开源的项目,它是Visual Studio的扩展.在使用Visual Studio开发基 ...
- 基于 Vue.js 之 iView UI 框架非工程化实践记要 使用 Newtonsoft.Json 操作 JSON 字符串 基于.net core实现项目自动编译、并生成nuget包 webpack + vue 在dev和production模式下的小小区别 这样入门asp.net core 之 静态文件 这样入门asp.net core,如何
基于 Vue.js 之 iView UI 框架非工程化实践记要 像我们平日里做惯了 Java 或者 .NET 这种后端程序员,对于前端的认识还常常停留在 jQuery 时代,包括其插件在需要时就引 ...
随机推荐
- Javascript 机器学习的四个层次
简介: Atwood定律说,凡是可以用Javascript实现的应用,最终都会用Javascript实现掉.作为最热门的机器学习领域,服务端是Python的主场,但是到了手机端呢?Android和i ...
- Roslyn 通过 EmbedAllSources 将源代码嵌入到 PDB 符号文件中方便开发者调试
本文来告诉大家如何在项目文件里面添加上 EmbedAllSources 属性,将自己的代码嵌入到 PDB 符号文件里面,让开发者们在调试的时候,可以看到库的源代码 是否记得 PDB 符号文件的作用?符 ...
- vue+element设置选择日期最大范围(普通版)
效果是只能跟当天时间有关(30天),下一篇将来的任意时段,比较符合实际 <!DOCTYPE html> <html> <head> <meta charset ...
- 记录一个vue路由拦截效果的小技巧
使用一句三元表达式, <router-link class="flex-left left" tag="div" :to="loginState ...
- iceoryx源码阅读(一)——全局概览
一.什么是iceoryx iceoryx是一套基于共享内存实现的进程间通信组件. 二.源码结构 iceoryx源码包括若干工程,整理如下表所示: 下图展示了主要项目之间的依赖(FROM:iceoryx ...
- three.js 物体要使用光线投射技术,计算是否点击位置与物体有交叉
原生 DOM 还用原生的 DOM 点击事件,要注意开启 pointerEvents CSS3DRenderer 是一个新的渲染器,需要在渲染循环调用并适配 labelRenderer.domEleme ...
- 关于UE4对象静态/动态的销毁问题整理(AddToRoot、TWeakObjectPtr)
1.非UObject对象 即非UObject常规C++对象,创建销毁不赘述.但可以用智能指针:从而不用关心销毁逻辑: TSharedPtr<ClassA> MyObj = MakeShar ...
- Chrome 浏览器插件 V3 版本 Manifest.json 文件中 Action 的类型(Types)、方法(Methods)和事件(Events)的属性和参数解析
一.类型(Types) 一.OpenPopupOptions 1. 属性 windowId: number 可选 打开操作弹出式窗口的窗口 ID.如果未指定,则默认为当前活动窗口. 二.TabDeta ...
- C语言:实现数组的删除和增加
/* 删除方法: 如:12 32 56 84 95 用覆盖的方法 若删除第三个,则第四个要覆盖第三个,第五个要覆盖第四个 覆盖完:12 32 84 ...
- 移动通信网络中的 GTP 协议
目录 文章目录 目录 GTP GTP-C 协议(GTP 控制面) GTP-U 协议(GTP 用户面) GTP' 协议(计费传输) GTPv2 Header GTP GTP(GPRS Tunnellin ...