照片由 Michael Dziedzic 拍摄,来自 Unsplash
想象你正在开发一个.NET应用程序,一开始一切正常。然而,突然出现了意想不到的运行时错误——尽管你的主项目并没有显式包含任何额外的包。这种现象该如何解释呢?通常,这种现象的原因是隐含依赖,比如Entity Framework Core可能会通过另一个NuGet包悄悄地被添加到你的项目中。在复杂的应用场景中,这可能导致版本冲突甚至难以诊断的运行时错误,比如 MissingMethodException
。如果没有针对性地管理这些依赖关系,解决这些问题可能需要花费很大的精力。
在多层结构的解决方案中,例如,项目 A 直接依赖项目 B,项目 B 又使用项目 C。因此,项目 A 就间接引用了项目 C——即使项目 C 并未在 .csproj
文件中明确列出。
使用外部NuGet包时,问题在于这些包是由引用的其他包带入的,但不会直接显示在主项目的 .csproj
文件中。比如说,假设项目A使用了包 PackageX
,该包内部引用了某个特定版本的EF Core,那么EF Core会作为传递依赖被包含进来——即使项目A从未明确地加入过它。
這些微妙的依賴關係鏈在大型項目中可以迅速產生而沒有被注意到,不是很驚人嗎?
在 .NET (一个微软的开发框架) 中常见的一个场景在许多 .NET 应用程序中,Entity Framework Core (EF Core) 用作数据访问库。即使主项目没有直接引用 EF Core,它也可能通过其他工具或数据访问包作为间接依赖项隐式包含。最初,一切似乎都运行得很好——EF Core 提供了一个稳定的 API 接口。然而,在更新 .NET 框架或添加新功能时,可能会遇到版本冲突问题,最终导致难以排查的运行时错误。
详细的解决方案为了防止不必要的包扩散,有两种主要方法:一是通过使用 PrivateAssets="all"
有针对性地限制单个包的传播,二是通过设置 <DisableTransitiveProjectReferences>true</DisableTransitiveProjectReferences>
来完全禁用传递性项目依赖。下面将对这两种方法进行分析。
PrivateAssets="all"
在 PackageReference
中添加属性 PrivateAssets="all"
,这样就可以防止相应的库传递给依赖树中的其他项目。这样一来,该包及其所有依赖项仅在当前项目中可用。
有好处:
- 粒度:您可以为每个包单独决定是否继承依赖项。
- 避免副作用:有问题的库不会被自动传递到子项目中。
缺点:
- 增加配置工作量:如果另一个项目也需要相同的库,则必须再次明确引用。
比如说 :
<ItemGroup>
<PackageReference Include="SomeDataAccessPackage" Version="1.2.3" PrivateAssets="all" />
</ItemGroup>
在复杂环境中,特定包的配置工作所花费的时间和资源是否超过了其带来的好处?
禁用传递性项目引用关系在 .csproj
文件中设置 <DisableTransitiveProjectReferences>true</DisableTransitiveProjectReferences>
,你可以防止所有传递项目引用的自动传播。每个项目都必须明确列出所有需要的包。
好处:
提高透明性:所有依赖项都清晰可见且易于控制。
避免意外包含:项目中不会自动添加任何隐式的库,以避免意外包含。
缺点:
更高的维护工作量:特别是在大项目里,明确列出来所有的依赖项可能会让管理工作变得特别费劲。
遗漏的可能性:可能会不小心遗漏一些重要的依赖关系,从而引发编译错误或运行时错误。
例子:
<PropertyGroup>
<DisableTransitiveProjectReferences>true</DisableTransitiveProjectReferences>
<!-- 禁用传递性项目引用,即不启用项目之间的间接引用 -->
</PropertyGroup>
考虑到这些额外的努力,明确指出所有的依赖关系真的实际吗?
来看看这两种方法有什么不同粒度:这是一个术语,表示颗粒的大小或精细度。
使用 PrivateAssets="all"
,控制是在每个单独包的级别上应用的。这意味着你可以精确地控制哪些依赖不应传递给子项目。相比之下,DisableTransitiveProjectReferences
选项会全局禁用所有传递引用。这样一来,所有依赖都必须显式地引用,你无法单独管理各个包的依赖。
维护努力:
使用 PrivateAssets="all"
需要在每个项目中单独进行调整,以满足特定的依赖需求。这在处理大量包时会增加工作量。另一方面,启用 DisableTransitiveProjectReferences
通常会增加更高的维护工作量,因为所有引用都必须分别管理和更新,这在大型项目中可能带来额外的管理负担。
清晰度:
启用 DisableTransitiveProjectReferences
的一个好处是它能明确地区分依赖关系:所有使用了的包都明确列出,使得追踪这些包变得更容易。然而,这种方法也存在遗漏重要包的风险,如果没有显式添加这些包。相比之下,当设置为 PrivateAssets="all"
时,在复杂结构中,依赖链可能会变得不清晰,因为一些内部使用的包不会出现在项目文件中。
灵活的:
关于灵活性,PrivateAssets='all' 让你可以针对每个包做出具体的决策,这是一个优势。这种细粒度的控制让你可以有意地隔离特定的依赖。相比之下,DisableTransitiveProjectReferences 提供了更严格的控制,然而,这样做会牺牲灵活性,因为你必须手动维护和引用所有包。
实际例子:抽象和接口封装:总的来说,这两种方法各有优缺点。决策主要取决于具体项目对粒度、维护的难易程度、易理解性和调整的灵活性的需求。
考虑一个具体的场景:一个名为 Common 的 .NET Standard 库封装了各种辅助方法和工具,并且内部包含一个数据访问包,该包将 EF Core 作为传递依赖项。在这种情况下,建议不要在公共 API 中暴露 EF Core 特定的类;而是应该通过接口来抽象这些类。
示例实现:
// 公共部分
public interface IDataService
{
IEnumerable<string> GetData();
}
internal class EfDataService : IDataService
{
public IEnumerable<string> GetData()
{
// 使用EF Core内部实现
return new List<string> { “来自EF Core的数据” };
}
}
public static class DataServiceFactory
{
public static IDataService 创建数据服务()
{
return new EfDataService();
}
}
Common.csproj 中的配置项:
<ItemGroup>
<PackageReference Include="SomeDataAccessPackage" Version="1.2.3" PrivateAssets="all" />
</ItemGroup>
这种方法保证EF Core仅在内部使用,主应用程序只需与接口交互,无需了解内部实现。
高级策略和替代方案除了上述提到的配置选项外,还可以采取其他策略来减少版本冲突和间接依赖性带来的问题。
绑定重定向:
注:若此短语是专有名词或有特定含义,请提供更多信息以便更准确翻译。根据上下文,建议翻译为:
绑定重定向:
尤其是较旧的.NET Framework应用中,绑定重定向配置可以帮助解决不同版本的同一库之间的冲突。
模块化架构:
将应用程序划分为明确定义的模块,可以有针对性地隔离依赖关系,从而更容易控制不需要的副作用。
我们是否应该质疑,在非常复杂的系统中,仅仅关注.csproj配置是否真的是最好的解决方案?
简单总结传递依赖在复杂的 .NET 项目中是一个严重的问题。看似微不足道的库,比如 EF Core,通过其他 NuGet 包引入时,可能会导致严重的版本冲突和运行时错误。一致地分离依赖并有针对性地使用诸如 PrivateAssets="all"
或 <DisableTransitiveProjectReferences>
等措施,提供了实用的解决方案。必须权衡管理开销是否值得这些好处,以及如何确保长期的可维护性。
通过使用抽象,明确引用以及诸如绑定重定向等额外措施,开发人员不仅能创建更稳定的代码库,还能减少持续维护的负担。定期使用 dotnet list package – include-transitive 或 NDepend 等工具审查依赖层次结构是非常重要的。
每个开发者难道不应该不断质疑当前架构是否仍然满足现代应用程序不断增长的需求吗?
我觉得是的……
共同學(xué)習(xí),寫(xiě)下你的評(píng)論
評(píng)論加載中...
作者其他優(yōu)質(zhì)文章
100積分直接送
付費(fèi)專(zhuān)欄免費(fèi)學(xué)
大額優(yōu)惠券免費(fèi)領(lǐng)