10 月 23 - 25 日,QCon 上海站即将召开,现在购票,享9折优惠 了解详情
写点什么

管理大规模.NET 应用程序共享库的最佳实践

作者:Sergio Vanin

  • 2025-05-24
    北京
  • 本文字数:6801 字

    阅读完需:约 22 分钟

大小:2.96M时长:17:16
管理大规模.NET应用程序共享库的最佳实践

篇文章讨论了在现实世界中使用共享库的案例、影响以及可能的解决方案,以便解决在许多相互依赖的项目中使用它们时遇到的问题。

 

这里提到的挑战和解决方案主要聚焦.NET 项目,但建议的解决方案也适应于其他技术和语言。

 

本文还建议,在创建和使用共享库之前,软件架构师和开发人员应进行分析权衡。

 

理解共享库

 

共享库是为执行多个项目中都存在的任务而设计的可重用代码包。它们可以节省时间,确保一致性,让开发人员不用重新发明轮子。

 

这些库也被称为客户端库或包。它们可以是内部的或公共的,可以进行版本控制(使开发人员能够控制更改),并且需要有管理它们并分发到不同项目的方法。

 

共享库的最佳用例

 

使用共享库的主要动机是标准化和简化开发流程。然而,我们必须认识到,在决定创建和使用共享库时,要做一些权衡,正如Ben Morris所展示的那样。以下是我们的总结:

 

通常会考虑的一些好处包括:

  • 效率提升和代码质量

  • 防止重复工作和标准化解决方案

  • 更好的模块化和抽象

  • 改善协作

 

但共享库也经常会带来意料之外的问题:

  • 增加团队之间的耦合

  • 引起版本兼容性挑战

  • 潜在的破坏性更改或回归风险

 

共享库管理

 

一旦组织决定将共享库作为其架构策略的一部分,下一步就是创建一个有效的方法在多个微服务中分发和管理这些库。通常,这是通过托管内部工件存储库来实现的。像 AWS CodeArtifact、JFrog Artifactory 等解决方案通常就是用于这个目的的。这些系统允许团队发布和消费包(例如 NuGet for .NET),确保共享组件是可访问的并且受版本控制。

 

特别是在.NET 生态系统中,NuGet 包是管理和分发共享库的标准,使团队能够轻松地集成不同服务共有的功能。不过,虽然维护和发布共享库本身的更新是一项可管理的任务,但是,确保所有依赖共享库的服务都能一致地采用这些更新却是一项真正的挑战。

 

随着系统环境的扩展,分散在服务中的过时依赖项可能会迅速从一个小麻烦演变成一个关键的操作风险。这些陈旧的版本可能导致行为不一致,暴露安全漏洞,并导致服务之间的集成问题。如果放任不管,这种情况将成为一个扩展瓶颈,使部署变得复杂,并减慢整体交付流程。

 

现实世界的挑战

 

为了理解在不同项目中管理共享库版本的复杂性,让我们分析以下几个涉及.NET 项目内部共享库的真实案例。

 

现实世界场景:在 AWS Lambda 中管理认证

 

  • 场景:团队开发了一个共享库来处理由 AWS API 网关端点触发的针对 10 个 Lambda 函数的认证逻辑。这些函数使用.NET 8 构建,并使用共享库来验证请求数据的客户和用户是否可信。使用共享库的目的是为了整合共有的功能,并减少代码重复。然而,随着该库的演进,它现在不仅包括认证类,还包括 API 内容协商类和错误处理逻辑,这就引入了跨多个服务的额外依赖项。

  • 挑战:在开发过程中引入了一个新的认证参数,就需要在部署之前更新所有依赖这个共享库的 Lambda 函数。

  • 影响:这种依赖减缓了新功能的推出速度,突显了紧耦合服务的风险。如果它已经投入生产应用,要推出新功能,无疑就会破坏一些已经在使用这些端点的外部 API。

 

RabbitMQ 集成:一个可扩展性挑战

 

  • 场景:在这种情况下,一个特定的内部共享库被构建了出来,用于处理 RabbitMQ 的连接和队列。大约有 50 个微服务在 Kubernetes 集群下运行,而且必须与 RabbitMQ 集成。所有微服务都由同一个团队管理。这项决策的背景是,一家小公司将其系统从单体架构重写成了许多微服务,但在投入生产应用后很少修改。

  • 挑战:必须将 RabbitMQ 版本从 3.13.7 升级到 4.0.4,以便能够支持并访问新的队列模型,特别是仲裁队列。然而,这次升级引入了一个破坏性更改:队列声明现在需要一个新的参数,这与当前的共享库和所有现有的微服务实现都不兼容。

  • 影响:存在一个死锁情况:如果 RabbitMQ 在微服务之前升级,所有服务在尝试连接或创建队列时都会失败。如果微服务在 RabbitMQ 之前升级,它们会尝试使用当前 RabbitMQ 实例尚不支持的仲裁队列,导致消息流立即中断。这就产生了一个零容忍的迁移窗口,要求所有服务同步升级。鉴于当前的资源限制,这是一个复杂度很高且风险很大的操作。向 RabbitMQ 4.0.4 的升级已被推迟,直到团队找到同时迁移所有微服务的最佳方式。没有清晰且可执行的迁移策略,团队就无法采用关键的 RabbitMQ 改进。这增加了技术债务,而且,如果升级或变更库时协调不当,就会增加出现灾难性中断的风险。

 

洞见:对共享库的依赖可能成为可扩展场景中的重大阻碍。

 

保持服务与共享库变更同步

 

1.手动更新:传统方法

 

要处理所有依赖共享库的服务的更新,我们首先想到的方法是手动管理它们。每个服务都应该有一个团队负责维护并将其纳入开发流程,以保持最新状态。

 

然而,使用这种方法的话,可能需要很长时间才能更新完所有服务,因为所有团队必须保持一致,每个团队都要将这项任务作为待办列表中的优先事项。这个过程可能既耗时又容易出错。

 

2.集中化的依赖版本管理

 

手动方法的一个替代方案是以集中化的方式管理服务所依赖的共享库,考虑下外部文件或处理该问题的工具/功能。像这样的工具,以及包括持续集成/持续部署(CI/CD)管道(提供回归测试和自动化部署)在内的既定流程,有助于实现这些预期。

 

在更新共享库时,总是存在新版本引入破坏性变更或意外行为的风险。回归测试可以确保更新完成后现有功能保持不变,减少在集成库的新版本时引入新 bug 的机会。

 

动态管理依赖意味着依赖共享库的服务可以频繁更新。自动化部署管道可以确保这些更新高效、一致、无误,尽可能减少手动干预并降低部署风险。

 

对于实现集中化依赖版本控制,带有回归测试和自动化部署的 CI/CD 管道至关重要。它们有助于在依赖更新到达生产环境之前对其进行验证。它们实现了以下任务的自动化:

  • 运行回归测试

  • 检查兼容性问题

  • 最小化新版本部署停机时间

 

其思想是加快更新依赖项目的过程,快速完成新功能的启用或工具的升级。

 

通常,开发团队会在项目开始时决定结构:他们将使用单个存储库存储代码(称为单体存储库),有一个解决方案包含一个或多个项目,但都保存在同一个存储库中,或者他们将使用多个存储库来保存代码,创建包含相关项目的不同解决方案,每个解决方案按领域/职责划分。

 

对于这些决策中的任何一个,我们都可以使用.NET 中名为中央包管理(CPM)的功能。这是.NET 6+的一个功能,可以帮助管理一个解决方案中所有项目的依赖项。它在一个地方管控库和版本,使这项工作变得更容易。在文章的剩余部分中,我们将称之为 CPM。

 

3.基于存储库结构的策略

 

使用 CPM 的单体存储库策略

 

使用 CPM 时,我们可以在单个文件(Directory.Package.props)中管理所有包版本,该文件必须与解决方案文件(.sln)位于相同的文件夹中。.props 文件包含所有的包(名称和版本),每个项目文件通过名称引用需要包含在该项目中的包。此外,如果其中一个项目需要使用与.props 文件中定义的版本不同的版本,那么它可以明确地覆盖那个版本。

 

正如我们所看到的,CPM 主要用于管理包含多个项目的单一解决方案的依赖项。它简化了包版本管理,因为你可以在单个文件中集中指定版本。这可以提高解决方案的性能,因为所有项目都引用相同的包版本,减少了不必要的还原和下载。此外,它还减少了维护工作量,因为更新包版本只需要在单个文件中进行。

 

多存储库策略:CPM 搭配 Git 子模块

 

前文介绍的真实案例涵盖了不同的领域,但在这两种情况下,每个微服务都有自己的解决方案和相应的 git 存储库。当出现像 RabbitMQ 集成案例这样的情况时,多个解决方案使用相同的内部共享库,建议的方法是将 CPM 与 git 子模块结合起来,所有解决方案共用单个 Directory.Package.props 文件。

 

正如Git文档所描述的那样,“子模块允许你将一个 Git 存储库作为另一个 Git 存储库的子目录。这让你可以将另一个存储库克隆到你的项目中,而提交依然是分开的”。

 

例如,想象你正在构建一个后端应用程序,它依赖于在单独的 Git 存储库中开发的共享认证模块。如果将认证代码复制到自己的应用程序中,就会增加跟踪更改或获取更新的难度,在这种情况下,你可以使用 git submodule add 将其添加为 Git 子模块。这样就可以将外部存储库拉取到你的项目中作为一个子目录,同时保持其提交历史和版本控制独立。然后,你可以在应用程序中引用认证模块的特定版本,在需要时更新它,并保持核心后端逻辑与共享组件之间的清晰隔离,就像在多个微服务中使用常见的内部库一样。

 

这里的主要思想是,一个存储库只存储 CPM 使用的依赖项属性文件(不是一个包含代码的项目或解决方案,而只是依赖项属性文件)。相比之下,每个服务的存储库只保留其代码,因此当我们拉取存储库时,它可以正常工作。我们希望将这两个存储库作为独立的存储库对待,但你仍然能够在一个存储库中使用另一个。

 

实现这个想法的第一步是创建一个空存储库,其中只包含一个名为 Directory.Packages.props 的文件。这个文件将保存其他解决方案使用的所有包和版本。该文件内容如下:

<Project>  <PropertyGroup>    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>  </PropertyGroup>  <ItemGroup>    <PackageVersion Include="Newtonsoft.Json" Version="13.0.1" />    <PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="8.0.6" />    <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.3.0" />  </ItemGroup></Project>
复制代码

 

然后,我们就可以有一个存储库,其中包含解决方案和项目代码。在下面的示例中,我们可以看到解决方案的结构,它有两个项目 sharedlibrarypost 和 sharedlibraryui。每个项目都有一个 Directory.Packages.props 文件。

 

在这些项目中,.props 文件只是引用主 Directory.Packages.props 文件(包含包名称和版本)。如下所示是这些.props 文件的内容:

<Project>  <Import Project="..\packagereferences\Directory.Packages.props" /></Project>
复制代码

 

在这个解决方案中,我们可以通过 git 命令将第一个存储库作为 git 子模块包含进来。为了实现这一点,在解决方案文件夹中,我们可以使用以下命令:

git submodule add {.props github address} {folder}
复制代码

 

例如,git submodule add github/example/centralizedpackages packagereferences,其中,github/example/centralizedpackages 是.props 的 github 地址,packagereferences 是文件夹。

 

运行该命令后,解决方案结构如下所示:

- /sharedlibrarypost  ├── sharedlibrarypost.sln  ├── packagereferences    │ └── Directory.Packages.props  ├── sharedlibrarypost    ├── Directory.Packages.props    ├── Program.cs    ├── appsettings.json    ├── sharedlibrarypost.csproj    └── sharedlibrarypost.http  └── sharedlibraryui    ├── Directory.Packages.props    ├── Pages    ├── Program.cs    ├── appsettings.json    ├── sharedlibraryui.csproj
复制代码

 

准备好上述结构之后,我们必须始终运行以下 git 命令来构建解决方案,并将同样的命令作为构建管道的一部分:

git submodule update –init –remotedotnet build
复制代码

 

要使用这种方法,建议 CI/CD 管道至少要有构建、测试和发布阶段。测试非常重要,而且必须包括单元测试、集成测试和回归测试,全面覆盖所有组件。当提交中央 Directory.Packages.props 文件的新版本时,就会触发依赖项目的管道,解决方案将拉取最新版本进行构建。管道运行之后,开发人员就可以知道升级后的包版本是否破坏了什么构建或测试。

 

Directory.Packages.props 的新版本是通过管道输出自动反映在依赖服务中的,因此在本地使用子模块时需要注意,开发人员必须显式运行子模块更新才能将最新版本合并到本地。在实践中,开发人员忘记拉取和提交更新后的子模块引用,或继续使用过时版本而不自知。这种疏忽可能会引入不一致性并减慢开发工作流程。

 

4. 总包策略

 

中央包管理和 Git 子模块的一种替代方法是使用总包(也称为元包)。这种技术涉及创建一个专用的 NuGet 包,分组并声明依赖的其他内部或外部包及具体的版本。

 

通过引用这个总包(umbrella package),每个项目隐式地引入了所有必要的、预先定义好版本的依赖项。这种方法实现了依赖控制的集中化,而且无需管理多个配置文件或存储库。

 

例如,不用每个微服务都显式引用像 MyCompany.Logging、MyCompany.Security 或 Newtonsoft.Json 这样的公共包,它们只需引用一个总包,如 MyCompany.Platform。

 

这个总包的.nuspec 文件定义了如下依赖项:

<dependencies>  <dependency id="MyCompany.Logging" version="[1.2.3]" />  <dependency id="MyCompany.Security" version="[4.5.6]" />  <dependency id="Newtonsoft.Json" version="[13.0.1]" /></dependencies>
复制代码

 

总包发布以后,我们看一个需要三个共享库的微服务项目:MyCompany.Logging、MyCompany.Security 和 Newtonsoft.Json。下面,我们看看每种情况下项目文件的内容:

 

没有总包的情况下:

<ItemGroup>  <PackageReference Include="MyCompany.Logging" Version="1.2.3" />  <PackageReference Include="MyCompany.Security" Version="4.5.6" />  <PackageReference Include="Newtonsoft.Json" Version="13.0.1" /></ItemGroup>
复制代码

 

使用总包的情况下:

<ItemGroup>  <PackageReference Include="MyCompany.Platform" Version="2.0.0" /></ItemGroup>
复制代码

 

使用总包,项目只需要引用一个包(MyCompany.Platform),其中包含了所有指定版本的必需依赖项。这减少了项目文件的混乱,促进了一致性,并简化了版本管理。

 

当依赖项的新版本发布时,只需要更新总包并重新发布。不过,需要强调的是,消费/依赖项目仍然需要更新他们引用的总包版本,这样才从这些更新中受益。虽然这个过程不是自动的,但它简化了维护工作,因为只需要管理总包的版本,而不是多个单独的包引用。

 

注意:尽管总包实现了依赖管理的集中化,但要接收最新的依赖项,消费者解决方案必须手动更新他们引用的版本。

 

优点:

  • 简化了多个存储库之间的依赖管理。

  • 降低了服务之间版本不一致的风险。

  • 保持了项目文件的整洁,因为它们只引用总包。

  • 避免了管理共享配置文件或子模块。

  • 促进了标准化和内部依赖控制。

 

考虑因素:

  • 需要定期维护和发布总包。

  • 项目必须更新他们引用的总包版本才能接收到依赖项的更新。

  • 如果消费者项目也独立引用包,可能会有版本冲突的风险。

 

5. 对比分析

 

CPM 和总包的取舍

 

总包和中央包管理(CPM)都旨在简化跨项目的依赖版本控制,但它们在方法和适用性上有所不同,这取决于架构。

 

每种方法都有自己独特的优势和局限,可以通过不同的方式来匹配团队工作流程、存储库结构和发布实践。

 

明确列出它们的优缺点有助于团队根据他们的具体情况做出明智的架构决策,而不是仅基于技术熟悉度或便利性来选择要采用的工具。

方面

总包

中央包管理(CPM)

存储库结构

适用于多存储库环境;不需要跨存储库的共享文件。

最适合单体存储库或关系紧密的解决方案;需要共享配置文件(例如Directory.Packages.props)。

项目引用

项目只引用总包,保持项目文件的整洁和最小化。

项目单独引用包,但版本来自中央文件。

版本更新

要采用新的依赖项,需要在每个消费者中更新总包版本。

需要更新中央.props文件,并确保所有的消费者都拉取最新版本(使用Git子模块或类似策略)。

自动化

可以利用像Dependabot或Renovate这样的工具,自动提升消费者中总包的版本。

当中央.props文件更新并被消费时,更新就会传播,但在多存储库设置中需要Git子模块同步。

灵活性

在服务需要相同包的不同版本时,灵活性较小。

更细粒度的控制;如果需要,单个项目可以覆盖版本。

维护开销

需要维护和发布总包作为内部源的一部分。

需要维护.props文件,并确保在存储库之间同步,特别是在多存储库场景中。

 

在多存储库环境中,当最小化个别项目中的配置开销至关重要时,使用总包。这种方法非常适合那些喜欢引用单个元包的团队,在其中封装所有必需的依赖项,提升一致性,而且无需管理多个包引用。当存储库独立性是优先考虑事项,并且团队习惯于手动更新总包版本以引入变更时,这特别有用。使用总包就不需要共享配置文件或 Git 子模块了,它提供了一种整洁且标准化的方式来管理不同服务之间的依赖项。

 

在单体存储库设置或紧耦合的存储库结构中,当需要对包版本进行细粒度控制时,使用中央包管理(CPM)。CPM 允许团队通过 Directory.Packages.props 文件集中管理所有依赖项,简化更新,并确保同一存储库内多个项目之间的一致性。这种方法简化了维护工作,因为版本更新只需要一个更改,并且非常适合那些优先考虑在统一代码库中实现强一致性和高效依赖管理的团队。

 

是多存储库环境,但仍寻求跨存储库的包版本集中控制,使用 CPM 与 Git 子模块。通过将 CPM 与 Git 子模块相结合,团队可以在多个存储库之间共享集中化的 Directory.Packages.props 文件,确保依赖项版本的一致性,同时保持存储库的自主性。这种方法需要严格的工作流程来保证子模块在本地和 CI/CD 管道中的更新,但它在集中化的版本管理和灵活的分布式开发实践之间实现了很好的平衡。

 

总结

 

共享库可以加速开发,而如果管理不当,也可能成为一个明显的扩展瓶颈。为了减轻这些风险,团队应该采取适合其存储库架构的结构化策略。

 

中央包管理(CPM)为单体存储库设置提供了一种更为简单的方法,允许使用单个配置文件进行集中化版本控制。在多存储库环境中,将 CPM 与 Git 子模块集成既可以实现集中控制,还能保持存储库的独立性。不过,这种方法需要有严格的工作流程和一致的 CI/CD 管道支持。

 

另外,总包提供了一个与存储库无关的解决方案,将所有依赖项封装到单个包中,可以实现更简单的集成。然而,这种方法需要手动更新包引用。

 

借助这些策略、自动化 CI/CD 管道以及严格的回归测试,团队可以在有效管理共享依赖的同时,保证系统的稳定性和可扩展性。


原文链接:

https://www.infoq.com/articles/shared-libraries-dotnet-enterprise/

2025-05-24 12:005615

评论

发布
暂无评论

阿里大佬力荐6篇实战文档:JVM+多线程+Kafka+Redis+Nginx+MySQL,你确定不看?

收到请回复

Java 云计算 开源 架构 编程语言

“双减”一年,如何让教育回归本质?

旺链科技

区块链 产业区块链 企业号九月金秋榜 教培行业

WorkPlus移动应用管理平台 | 政企数字化的超级“连接器”

BeeWorks

私有化的即时通讯工具能为企业带来哪些帮助?

BeeWorks

新零售数智化转型,需要怎样的数据底座?

OceanBase 数据库

从零到一构建完整知识体系,阿里最新SpringBoot原理最佳实践真香

程序员小毕

Java spring 源码 面试 SpringBoot 2

关于用户 email 邮件地址是否允许有加号的问题

汪子熙

typescript 正则表达式 邮件 9月月更 输入校验

一文看懂:什么是CRM系统?有什么用?哪些公司在用?

优秀

CRM系统

FreeRTOS记录(八、用软件定时器?还是硬件定时器?)

矜辰所致

软件定时器 FreeRTOS 9月月更

SpringBoot源码 | refreshContext方法解析

六月的雨在InfoQ

源码 springboot 源码阅读 9月月更 refreshContext

Databend 特性系列(1)|Databend 数据生命周期

Databend

大数据 大数据 开源 数据生命周期

变革加速,博睿数据赋能“中国智造”转型升级

博睿数据

可观测性 智能运维 博睿数据

华为云快成长直播间大数据&AI专场,加速经济物联网智能化提升

科技怪咖

算法基础(二)| 高精度算法详解

timerring

算法 9月月更

7.07亿TPC-C背后的技术突破,OceanBase研究成果入选VLDB

OceanBase 数据库

【指针内功修炼】字符指针 + 指针数组 + 数组指针 + 指针参数(一)

Albert Edison

C语言 二维数组 9月月更 指针数组 数组指针

全新演绎!美团内部疯传Spring Boot速成手册也太香了叭!

收到请回复

Java 云计算 开源 架构 编程语言

Paper Time|开放式时空大数据助力智能公交路线规划

OceanBase 数据库

一文带你体验MRS HetuEngine如何实现跨源跨域分析

华为云开发者联盟

大数据 后端 企业号九月金秋榜

重磅!阿里首推内部“SpringCloudAlibaba项目文档”这细节讲解,封神!

收到请回复

Java 云计算 开源 架构 编程语言

“大厂”角力移动办公系统市场,钉钉和企微向左、WorkPlus向右

BeeWorks

面试造火箭!连续轰炸50问,我却靠这些"java复习宝典"一一攻克!

收到请回复

Java 云计算 开源 架构 编程语言

如何设计企业级数据埋点采集方案?

字节跳动数据平台

数据分析 用户增长 埋点 数据应用 埋点设计

带您了解昇腾模型压缩工具

华为云开发者联盟

人工智能 后端 企业号九月金秋榜

MobLink Android 快速集成文档

MobTech袤博科技

sdk Android;

推荐|海泰国密通信安全解决方案 助力用户实现安全合规

电子信息发烧客

百度App Android启动性能优化-工具篇

百度Geek说

android 性能优化 企业号九月金秋榜

高并发之缓存

源字节1号

软件开发

华为云快成长直播ERP专场,以数据驱动企业智慧变革

科技怪咖

小红书自研小程序:电商体验与效果优化的运行时体系设计

小红书技术REDtech

小程序 前端 小程序运行时

京东金融客户端用户触达方式的探索与实践

京东科技开发者

京东 用户 用户触达 widget 推送

管理大规模.NET应用程序共享库的最佳实践_编程语言_InfoQ精选文章