我们.NET开发会引用很多外部Nuget包,多项目、多个解决方案、甚至多个仓库。
简单的Nuget包管理,通过VS就能比较简单处理好。但复杂的场景呢,比如:
1.一个仓库里,有多个解决方案的Nuget包管理 -- 我现在项目就是这样的,针对会议大屏的全家桶软件集代码仓库。这个仓库里,接近30个工具/应用软件:
2. 多个仓库,多个解决方案、多个项目的Nuget管理 -- 公司产品项目业务比较复杂,抽了很多组件(目前有53个),组件间需要处理好版本冲突,以及因版本依赖需要递增升级自己的版本号
这些复杂场景会给开发同学带来操作管理代码的低效问题
所以如何高效处理好项目之间的Nuget版本冲突,如何快速升级Nuget版本,如何解决多项目对Nuget组件源码的快速调试,下面我们分别介绍下
Nuget版本统一、版本升级
项目Csproj引用Nuget包,管理多个项目的解决方案,通过VS的Nuget管理页面以及CsProj文件编辑能解决大部分情况下的Nuget版本操作。
稍复杂的仓库内多解决方案场景,经MVP德熙大佬 lindexi - 博客园 (cnblogs.com) 的提醒,中央包管理即Packages.props文件也能可以管理一个仓库内的Nuget包,中央包管理 | Microsoft Learn。Packages.props统一配置好仓库内公共的Nuget包版本,然后Csproj根据需要引用相应的Nuget包
1 <Project Sdk="Microsoft.NET.Sdk"> 2 <PropertyGroup> 3 <TargetFramework>net6.0</TargetFramework> 4 </PropertyGroup> 5 <ItemGroup> 6 <PackageReference Include="Newtonsoft.Json" /> 7 </ItemGroup> 8 </Project>
但是,其实我个人不推荐。如果新增了一个Nuget引用DirectShowLib,VS又会默认在CSproj文件内添加版本号,未自动归纳到Package.props内:
需要按中央包管理路径,去解决方案或者仓库下Packages.props添加Nuget版本信息。此方案有一定的修改成本,个人也觉得项目的Nuget引用版本号不够显眼、阅读性会减弱
继续用csproj管理Nuget包,多项目之间的管理成本我们用工具解决: WindowsOrg/NugetEfficientTool: VisualStudio项目开发-Nuget操作效率工具 (github.com)
下图是使用Nuget版本工具,对全家桶应用集所在的文件夹进行Nuget版本检测:
按上图界面操作:
1. 项目路径,输入仓库代码所在的目录(也可以外层目录)、解决方案sln文件路径
2. 点击“开始检测”,会显示Nuget待修复信息。如上图,H3C.Setting.Camera存在很多csproj文件内版本是一致的,但与Nuget源最新版本不一致,需要升级
3. 点击“修复版本冲突”,弹出版本选择窗口,选择相应版本执行修复。执行后,csproj都会同步H3C.Setting.Camera版本为1.1.15
选择版本时,可以全选也可以指定Nuget同步版本号。
Nuget版本工具,是基于小伙伴博哥工具版本dotnet-campus/dotnetCampus.NugetMergeFixTool (github.com)优化改良的。工具目前已经实现的功能:
1. 同步一个解决方案下Nuget版本
2. 同步一个文件夹下多解决方案的Nuget版本
3. 支持设置Nuget源,同步升级Nuget版本(上方截图就是这个场景)
Nuget版本同步的原理,是获取路径下的所有csproj文件,解析出引用Nuget的PackageReference信息-Nuget名称、版本号。列出这些版本不同的Nuget,让开发人员选择需要降级或者升级的版本号,然后再次修改保存csproj文件的Nuget引用版本号。
获取一个csproj文档内所有PackageReference节点:
1 /// <summary> 2 /// 获取PackageReference名称对应的节点 3 /// </summary> 4 /// <param name="xDocument"></param> 5 /// <returns></returns> 6 private List<XElement> GetPackageReferenceElements(XDocument xDocument) 7 { 8 if (xDocument == null) 9 { 10 throw new ArgumentNullException(nameof(xDocument)); 11 } 12 var xElementList = new List<XElement>(); 13 var itemGroupElements = xDocument.Root.Elements().Where(x => x.Name.LocalName == "ItemGroup"); 14 foreach (var itemGroupElement in itemGroupElements) 15 { 16 xElementList.AddRange(itemGroupElement.Elements().Where(x => x.Name.LocalName == "PackageReference")); 17 } 18 return xElementList; 19 }
另外,针对多个仓库多个解决方案的组件集版本号同步场景,组件间版本相互依赖导致版本无法管理,即组件A依赖组件B、组件B依赖组件C,组件C有代码变更如何将依赖C的其它组件自动更新版本?目前组件间版本依赖混乱对windows应用的稳定有很大影响,开发人员通过手动操作也有很大的工作量
下面介绍下53个组件仓库70多个Nuget包是如何一键解决Nuget之间版本依赖的问题。Nuget版本工具结合Jenkins做了个版本号自动构建:
1. 组件集工作组下的所有组件仓库clone到本地,并行编译多个解决方案
2. 后台执行Nuget版本工具,csproj文件中Nuget自动升级到最高版本。同时csproj文件如有引用变更,则当前组件自身版本号新增一位构建号
3. 循环第2步,直到所有csproj文件内相同Nuget包的版本均一致
4. 保存归档Nuget包至服务器,Git Push所有组件仓库对csproj文件的变更
下图是Jenkins某次一键同步Nuget包版本的流程日志:
有需要的可以直接下载Nuget工具:Nuget工具_1.0.3.1012.exe,以命令行参数执行、一键升级目录下的所有组件版本号:NugetEfficientTool.exe D:\Gitlab-Company\Components
Nuget源代码替换
开发应用,遇到需要调试内部的Nuget源代码,一般我们是卸载Nuget包、然后引用源组件项目csproj。
这样操作,1-2个项目操作还好,遇上几十个项目需要替换Nuget源项目代码,效率就很低了
简单重复的事情,都要通过工具来简化,这里也是Nuget工具操作:
1. 项目路径,输入仓库代码所在的目录(也可以是多仓库目录),或者仓库解决方案.sln文件路径
2. 输入Nuget包H3C.Family.App对应的源代码csproj文件路径,或者csproj所在目录。输入后,会自动填充Nuget名称H3C.Family.App
3. 点击“替换”,替换所有csproj项目内H3C.Family.App的Nuget引用PackageReference,为H3C.Family.App的项目引用ProjectReference,就可以开始源代码调试了
代码调试完,可以点击“还原”,撤回Nuget源代码的替换。然后Nuget源代码可以提交代码、归档Nuget包给上层应用使用了。
是不是很方便呢?这里Nuget替换,目前版本支持:
1. 一个解决方案,对多个Nuget源的替换
2. 目录即多个解决方案,对多个Nuget源的替换
3. 支持多个相互之间依赖的Nuget包替换(自动识别依赖)和还原
支持目录下的Nuget替换,极大提升了多项目多仓库协同开发的效率,见某次的操作:
我所在项目是智能交互大屏会议场景的会议软件集(全家桶),应用软件太多了。有时候修复一个摄像头组件的BUG,人工操作的话需要打开每个解决方案、替换源代码,太浪费时间。
而通过Nuget工具一键替换所有的Nuget源代码,然后用VS解决方案管理器切换视图,可极快的打开项目进行源代码确认、调试,效率提升相差一个量级
这里的Nuget替换,小伙伴俊杰同学 J.晒太阳的猫 - 博客园 (cnblogs.com) 开发过一个初始版本的,初始版本是在VS内以扩展工具来运行,功能没有这个Nuget工具齐全。可惜我没有他的源代码,就按个人需求另外开发了一套exe的Nuget工具版本,扩展了很多功能,支持最新PackageReference
补充一下,csproj对Nuget以前默认是Reference,PackageReference引用更加简洁、高效,性能也有一定优化,建议大家用这个。可以在VS设置Nuget引用方式,详见官网:https://learn.microsoft.com/zh-cn/nuget/consume-packages/migrate-packages-config-to-package-reference
其中主要逻辑,
1. 解决方案,新增Nuget名称项目:
1 var solutionFileLines = File.ReadAllLines(_solutionFile).ToList(); 2 //添加Project 3 var previousProjectIndex = solutionFileLines.FindLastIndex(i => i.Contains(StartProjectRex)); 4 var previousProjectLine = solutionFileLines[previousProjectIndex]; 5 var solutionId = previousProjectLine.Replace(StartProjectRex, string.Empty).Substring(0, GuidLength); 6 var newProjectLine = $"{StartProjectRex}{solutionId}}}\") = \"{_nugetName}\", \"{_sourceProjectFile}\", \"{{{_newProjectId}}}\""; 7 solutionFileLines.Insert(previousProjectIndex + 2, "EndProject"); 8 solutionFileLines.Insert(previousProjectIndex + 2, newProjectLine); 9 //添加编译配置 10 var projectConfigStartIndex = solutionFileLines.FindIndex(i => i.Contains("ProjectConfigurationPlatforms")); 11 var projectConfigEndIndex = solutionFileLines.FindIndex(projectConfigStartIndex + 1, i => i.Contains("EndGlobalSection")); 12 solutionFileLines.Insert(projectConfigEndIndex, $"{{{_newProjectId}}}.Debug|Any CPU.Build.0 = Release|Any CPU"); 13 solutionFileLines.Insert(projectConfigEndIndex, $"{{{_newProjectId}}}.Debug|Any CPU.Build.0 = Release|Any CPU"); 14 solutionFileLines.Insert(projectConfigEndIndex, $"{{{_newProjectId}}}.Release|Any CPU.Build.0 = Release|Any CPU"); 15 solutionFileLines.Insert(projectConfigEndIndex, $"{{{_newProjectId}}}.Release|Any CPU.Build.0 = Release|Any CPU"); 16 File.WriteAllLines(_solutionFile, solutionFileLines, Encoding.UTF8);
2. Csprj文件,替换Nuget引用为源项目csproj的项目引用:
1 public ReplacedFileRecord ReplaceNuget() 2 { 3 var nugetInfoReferences = CsProj.GetNugetReferences(Document).ToList(); 4 var referenceElement = nugetInfoReferences.FirstOrDefault(x => CsProj.GetNugetInfo(x).Name == _nugetName); 5 if (referenceElement == null) 6 { 7 return null; 8 } 9 //获取Nuget引用信息 10 var replacedFileRecord = GetNugetReferenceInfo(referenceElement, nugetInfoReferences.IndexOf(referenceElement)); 11 //删除Nuget的引用 12 referenceElement.Remove(); 13 //添加源项目的引用 14 AddSourceReference(); 15 SaveFile(); 16 return replacedFileRecord; 17 }
一个csproj文件,可能会有多个替换Nuget的记录,这些记录用于后续的还原操作:
1 /// <summary> 2 /// 一个文件Nuget变更记录 3 /// </summary> 4 [DataContract] 5 public class ReplacedFileRecord 6 { 7 /// <summary> 8 /// Nuget名称 9 /// </summary> 10 [DataMember] 11 public string NugetName { get; set; } 12 /// <summary> 13 /// 文件名称 14 /// </summary> 15 [DataMember] 16 public string FileName { get; set; } 17 /// <summary> 18 /// 变更位置 19 /// </summary> 20 [DataMember] 21 public int ModifiedLineIndex { get; set; } 22 /// <summary> 23 /// Nuget版本 24 /// </summary> 25 [DataMember] 26 public string Version { get; set; } 27 [CanBeNull] 28 [DataMember] 29 public string TargetFramework { get; set; } 30 [CanBeNull] 31 [DataMember] 32 public string NugetDllPath { get; set; } 33 /// <summary> 34 /// Nuget引用方式 35 /// </summary> 36 [DataMember] 37 public string ReferenceType { get; set; } 38 }
Nuget工具:Nuget工具_1.0.3.1012.exe,最新版以及源代码详见:WindowsOrg/NugetEfficientTool: VisualStudio项目开发-Nuget操作效率工具 (github.com),欢迎大家提交优化一起贡献代码
关键字:Nuget包管理、Nuget源代码调试、NugetEfficientTool