"Copy Local"和项目引用的最佳实践是什么?
"Copy Local"和项目引用的最佳实践是什么?
我有一个大型的C#解决方案文件(大约100个项目),我正在尝试改进构建时间。我认为对于我们来说,“Copy Local”在许多情况下都是浪费的,但我想知道最佳实践是什么。
在我们的.sln文件中,应用程序A依赖于程序集B,而程序集B又依赖于程序集C。在我们的情况下,有几十个“B”和几个“C”。由于这些都包含在.sln文件中,我们使用项目引用。所有程序集当前都构建到$(SolutionDir)/Debug(或Release)中。
默认情况下,Visual Studio将这些项目引用标记为“Copy Local”,这导致每个构建“B”时,每个“C”都会被复制到$(SolutionDir)/Debug中一次。这似乎是浪费的。如果我关闭“Copy Local”,会出现什么问题?其他大型系统的人们是如何处理的?
跟进:
很多回答建议将构建拆分为较小的.sln文件...在上面的示例中,我首先构建基础类“C”,然后是大部分模块“B”,最后是几个应用程序“A”。在这种模型中,我需要从B到C的非项目引用。我遇到的问题是,“Debug”或“Release”被嵌入到提示路径中,结果我用调试版的C构建发布版的B。
对于那些将构建拆分为多个.sln文件的人来说,你们是如何处理这个问题的?
在项目引用中,最佳做法是什么?
在项目引用中,最佳做法是将“Copy Local”设置为false,除了依赖树的顶部项目。对于顶部项目中的所有引用,将“Copy Local”设置为true。很多人建议共享一个输出目录,但我认为这是一个根据经验得出的可怕的想法。如果启动项目引用了其他项目引用的dll,即使在所有项目上都设置了“Copy Local”为false,你也会遇到访问/共享违例,并且构建将失败。这个问题非常令人烦恼,很难追踪。我完全建议避免共享输出目录,而是将依赖链中的项目所需的程序集写入相应的文件夹。如果没有一个项目在“顶部”,我建议使用后期构建复制来将所有内容放在正确的位置。另外,我会尽量考虑调试的便利性。对于任何exe项目,我仍然将“Copy Local”设置为true,以便F5调试体验正常工作。
我有同样的想法,并希望能找到其他人也持同样观点的人;然而,我很好奇为什么这个帖子没有更多的赞同。不同意的人:你为什么不同意?
不可能发生这种情况,除非同一个项目构建了两次,如果只构建一次并且不复制任何文件,为什么会被覆盖/访问/共享违例?
没错。如果开发工作流需要在解决方案中构建一个项目,而另一个项目-可执行文件正在运行,将所有内容放在同一个输出目录中将会很混乱。在这种情况下,最好将可执行文件的输出文件夹分开。
在项目引用中,最佳做法是将“Copy Local”设置为false,除了依赖树的顶部项目。对于顶部项目中的所有引用,将“Copy Local”设置为true。避免共享输出目录,而是将依赖链中的项目所需的程序集写入相应的文件夹。对于任何exe项目,将“Copy Local”设置为true,以便F5调试体验正常工作。如果同一个项目构建了两次,会出现覆盖/访问/共享违例的问题。如果开发工作流需要在解决方案中构建一个项目,而另一个项目-可执行文件正在运行,最好将可执行文件的输出文件夹分开。
在这些文章中,作者提到了关于“Copy Local”和项目引用的最佳实践。作者指出,将"Copy Local"选项设置为True会显著增加编译时间,并且会影响工作环境。同时,这样做还会引入版本问题的风险。作者建议的正确做法是将项目引用的程序集放在Debug目录中,并配置Visual Studio项目将程序集输出到Debug目录中。此外,还有通过减少项目数量来改善编译时间的重要性。
根据以上内容,我们可以得出问题的原因是使用"Copy Local"选项会导致编译时间增加、工作环境混乱以及版本问题的风险。解决方法是将项目引用的程序集放在Debug目录中,并通过减少项目数量来改善编译时间。
文章链接:
- [Partitioning Your Code Base Through .NET Assemblies and Visual Studio Projects](http://www.simple-talk.com/dotnet/.net-framework/partitioning-your-code-base-through-.net-assemblies-and-visual-studio-projects/)
- [Lessons learned from the NUnit code base](http://codebetter.com/blogs/patricksmacchia/archive/2009/01/11/lessons-learned-from-the-nunit-code-base.aspx)
- [Analyzing the code base of CruiseControl.NET](http://codebetter.com/patricksmacchia/2009/03/15/analyzing-the-code-base-of-cruisecontrol-net/)
- [Controlling Dependencies](http://www.theserverside.net/tt/articles/showarticle.tss?id=ControllingDependencies)
在以前的项目中,我使用了一个大型解决方案,并遇到了性能问题。解决方案有三个部分:
1. 总是将Copy Local属性设置为false,并通过自定义msbuild步骤强制执行。
2. 为每个项目设置输出目录为相同的目录(最好是相对于$(SolutionDir))。
3. 默认的cs目标文件是使用框架一起提供的,它计算要复制到当前正在构建的项目的输出目录的引用集合。由于这需要计算“引用”关系下的传递闭包,因此这可能非常耗时。我的解决方法是在一个通用的目标文件(例如Common.targets)中重新定义GetCopyToOutputDirectoryItems目标,该目标在每个项目中的Microsoft.CSharp.targets导入之后导入。结果是每个项目文件看起来像下面的样子:
... snip ...
... snip ...
通过这种解决方法,我们将构建时间从几个小时(主要是由于内存限制)减少到几分钟。
这个办法中重新定义的GetCopyToOutputDirectoryItems可以通过将C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Microsoft.Common.targets中第2,438–2,450行和2,474–2,524行复制到Common.targets中来创建。
完整的目标定义如下:
Name="GetCopyToOutputDirectoryItems" Outputs="@(AllItemsFullPathWithTargetPath)" DependsOnTargets="AssignTargetPaths;_SplitProjectReferencesByFileExistence"> Include="@(ContentWithTargetPath->'%(FullPath)')" Condition="'%(ContentWithTargetPath.CopyToOutputDirectory)'=='Always' or '%(ContentWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'" > Include="@(_EmbeddedResourceWithTargetPath->'%(FullPath)')" Condition="'%(_EmbeddedResourceWithTargetPath.CopyToOutputDirectory)'=='Always' or '%(_EmbeddedResourceWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'" > Condition="'%(_EmbeddedResourceWithTargetPath.CopyToOutputDirectory)'=='Always'"/> Condition="'%(_EmbeddedResourceWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'"/> Include="@(Compile->'%(FullPath)')" Condition="'%(Compile.CopyToOutputDirectory)'=='Always' or '%(Compile.CopyToOutputDirectory)'=='PreserveNewest'"> Condition="'%(_CompileItemsToCopyWithTargetPath.CopyToOutputDirectory)'=='Always'"/> Condition="'%(_CompileItemsToCopyWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'"/> Include="@(_NoneWithTargetPath->'%(FullPath)')" Condition="'%(_NoneWithTargetPath.CopyToOutputDirectory)'=='Always' or '%(_NoneWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'" > Condition="'%(_NoneWithTargetPath.CopyToOutputDirectory)'=='Always'"/> Condition="'%(_NoneWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'"/>
有了这个解决方法,我发现在一个解决方案中有超过120个项目仍然可行,这主要的好处是项目的构建顺序仍然可以由VS确定,而不是通过手动拆分解决方案来确定。
是否只应将Copy Local设置为false用于项目引用还是所有引用?
至少对于项目引用,应将Copy Local设置为false。如果将其他(非GAC)依赖项添加到输出路径中,可以节省一些不必要的复制操作。
-1 对于无条件建议禁用CopyLocal。设置CopyLocal=false可能会在部署时导致不同的问题。请参阅我的博客文章"Do NOT Change "Copy Local" project references to false, unless understand subsequences."(geekswithblogs.net/mnf/archive/2012/12/09/…)
从Microsoft.Common.targets中可以看到,GetCopyToOutputDirectoryItems目标将获取可能需要传输到输出目录的所有项目项,这包括从传递引用的项目中获取的项。看起来,该目标计算了所有引用项目的内容项的完整传递闭包;然而,事实并非如此。它只收集其直接子项目的内容项,而不是子子项目的内容项。造成这种情况的原因是SplitProjectReferencesByFileExistence消耗的ProjectReferenceWithConfiguration列表只在当前项目中填充,并且在子项目中为空。空列表导致_MSBuildProjectReferenceExistent为空,并终止递归。所以它看起来是无用的。
我使用CustomAfterMicrosoftCommonTargets属性来避免更改MSBuild目标或VisualStudio项目。详情请参见simple-talk.com/dotnet/.net-tools/extending-msbuild
你能发表一下你用于强制在每个项目的每个引用中将CopyLocal设置为false的自定义构建步骤吗?
:很抱歉,因为我不再在那个项目上工作,也没有一个复制的检查的副本。抱歉。
:没问题,我在这里找到了一个答案:stackoverflow.com/questions/1682096/…。您介意将其链接到此答案吗?
似乎在输出路径设置中无法使用$(SolutionDir)变量。相反,我必须使用..\bin。无论如何,这对我来说是解决问题的最简单方法,所以谢谢你。
我们使用这个方法已经很多年了,但似乎它需要更新 - 在VS 2019下用dotnet build构建会导致由于_AssignTargetPaths未定义而出现错误。 🙁
:很抱歉,正如您所看到的,这个答案已经有将近10年的历史了,我想说自那时以来世界发生了很大的变化。