ChatGPT解决这个技术问题 Extra ChatGPT

“复制本地”和项目参考的最佳实践是什么?

我有一个大型 c# 解决方案文件(约 100 个项目),并且我正在尝试缩短构建时间。我认为“复制本地”在很多情况下对我们来说是浪费,但我想知道最佳实践。

在我们的 .sln 中,我们有依赖于程序集 B 的应用程序 A,它依赖于程序集 C。在我们的例子中,有几十个“B”和少数“C”。由于这些都包含在 .sln 中,因此我们使用的是项目引用。当前所有程序集都构建到 $(SolutionDir)/Debug(或 Release)中。

默认情况下,Visual Studio 将这些项目引用标记为“复制本地”,这会导致每个“C”被复制到 $(SolutionDir)/Debug 中,对于每个构建的“B”。这似乎很浪费。如果我只关闭“复制本地”会出现什么问题?其他拥有大型系统的人会做什么?

跟进:

许多响应建议将构建分解为较小的 .sln 文件...在上面的示例中,我将首先构建基础类“C”,然后是大部分模块“B”,然后是一些应用程序,“一个”。在这个模型中,我需要从 B 获得对 C 的非项目引用。我遇到的问题是“调试”或“发布”被纳入提示路径,我最终构建了“B”的发布版本针对“C”的调试版本。

对于那些将构建拆分为多个 .sln 文件的人,您如何处理这个问题?

您可以通过直接编辑项目文件使您的提示路径引用 Debug 或 Release 目录。使用 $(Configuration) 代替 Debug 或 Release。例如,..\output\$(Configuration)\test.dll 当您有很多引用时,这会很痛苦(尽管有人编写插件应该不难管理这个)。
Visual Studio 中的“复制本地”是否与 csproj 中的 <Private>True</Private> 相同?
但是将 .sln 拆分为更小的 <ProjectReference/> 会破坏 VS 对 <ProjectReference/> 的自动相互依赖性计算。我自己已经从多个较小的 .sln 转移到一个大的 .sln 只是因为 VS 以这种方式导致的问题更少……所以,也许后续假设是对原始问题的非必要最佳解决方案? ;-)
只是出于好奇。为什么要把事情复杂化并首先拥有 100 多个项目?这是糟糕的设计还是什么?
@ColonelPanic 是的。至少当我在 GUI 中更改该切换时,磁盘上会发生变化。

B
Bas Bossink

在之前的一个项目中,我使用了一个带有项目引用的大型解决方案,并且也遇到了性能问题。解决方案是三个方面:

始终将 Copy Local 属性设置为 false 并通过自定义 msbuild 步骤强制执行此操作 将每个项目的输出目录设置为同一目录(最好相对于 $(SolutionDir) 框架附带的默认 cs 目标计算引用集被复制到当前正在构建的项目的输出目录。由于这需要在“参考”关系下计算传递闭包,这可能会变得非常昂贵。我的解决方法是在公共目标文件中重新定义 GetCopyToOutputDirectoryItems 目标(例如.common.targets ) 在导入 Microsoft.CSharp.targets 后在每个项目中导入。导致每个项目文件如下所示: ... snip ...

这将我们在给定时间的构建时间从几个小时(主要是由于内存限制)减少到了几分钟。

可以通过将行 2,438–2,450 和 2,474–2,524 从 C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Microsoft.Common.targets 复制到 Common.targets 来创建重新定义的 GetCopyToOutputDirectoryItems

为了完整起见,生成的目标定义变为:

<!-- This is a modified version of the Microsoft.Common.targets
     version of this target it does not include transitively
     referenced projects. Since this leads to enormous memory
     consumption and is not needed since we use the single
     output directory strategy.
============================================================
                    GetCopyToOutputDirectoryItems

Get all project items that may need to be transferred to the
output directory.
============================================================ -->
<Target
    Name="GetCopyToOutputDirectoryItems"
    Outputs="@(AllItemsFullPathWithTargetPath)"
    DependsOnTargets="AssignTargetPaths;_SplitProjectReferencesByFileExistence">

    <!-- Get items from this project last so that they will be copied last. -->
    <CreateItem
        Include="@(ContentWithTargetPath->'%(FullPath)')"
        Condition="'%(ContentWithTargetPath.CopyToOutputDirectory)'=='Always' or '%(ContentWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'"
            >
        <Output TaskParameter="Include" ItemName="AllItemsFullPathWithTargetPath"/>
        <Output TaskParameter="Include" ItemName="_SourceItemsToCopyToOutputDirectoryAlways"
                Condition="'%(ContentWithTargetPath.CopyToOutputDirectory)'=='Always'"/>
        <Output TaskParameter="Include" ItemName="_SourceItemsToCopyToOutputDirectory"
                Condition="'%(ContentWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'"/>
    </CreateItem>

    <CreateItem
        Include="@(_EmbeddedResourceWithTargetPath->'%(FullPath)')"
        Condition="'%(_EmbeddedResourceWithTargetPath.CopyToOutputDirectory)'=='Always' or '%(_EmbeddedResourceWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'"
            >
        <Output TaskParameter="Include" ItemName="AllItemsFullPathWithTargetPath"/>
        <Output TaskParameter="Include" ItemName="_SourceItemsToCopyToOutputDirectoryAlways"
                Condition="'%(_EmbeddedResourceWithTargetPath.CopyToOutputDirectory)'=='Always'"/>
        <Output TaskParameter="Include" ItemName="_SourceItemsToCopyToOutputDirectory"
                Condition="'%(_EmbeddedResourceWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'"/>
    </CreateItem>

    <CreateItem
        Include="@(Compile->'%(FullPath)')"
        Condition="'%(Compile.CopyToOutputDirectory)'=='Always' or '%(Compile.CopyToOutputDirectory)'=='PreserveNewest'">
        <Output TaskParameter="Include" ItemName="_CompileItemsToCopy"/>
    </CreateItem>
    <AssignTargetPath Files="@(_CompileItemsToCopy)" RootFolder="$(MSBuildProjectDirectory)">
        <Output TaskParameter="AssignedFiles" ItemName="_CompileItemsToCopyWithTargetPath" />
    </AssignTargetPath>
    <CreateItem Include="@(_CompileItemsToCopyWithTargetPath)">
        <Output TaskParameter="Include" ItemName="AllItemsFullPathWithTargetPath"/>
        <Output TaskParameter="Include" ItemName="_SourceItemsToCopyToOutputDirectoryAlways"
                Condition="'%(_CompileItemsToCopyWithTargetPath.CopyToOutputDirectory)'=='Always'"/>
        <Output TaskParameter="Include" ItemName="_SourceItemsToCopyToOutputDirectory"
                Condition="'%(_CompileItemsToCopyWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'"/>
    </CreateItem>

    <CreateItem
        Include="@(_NoneWithTargetPath->'%(FullPath)')"
        Condition="'%(_NoneWithTargetPath.CopyToOutputDirectory)'=='Always' or '%(_NoneWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'"
            >
        <Output TaskParameter="Include" ItemName="AllItemsFullPathWithTargetPath"/>
        <Output TaskParameter="Include" ItemName="_SourceItemsToCopyToOutputDirectoryAlways"
                Condition="'%(_NoneWithTargetPath.CopyToOutputDirectory)'=='Always'"/>
        <Output TaskParameter="Include" ItemName="_SourceItemsToCopyToOutputDirectory"
                Condition="'%(_NoneWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'"/>
    </CreateItem>
</Target>

有了这个解决方法,我发现在一个解决方案中拥有多达 120 个以上的项目是可行的,这主要的好处是项目的构建顺序仍然可以由 VS 确定,而不是通过拆分解决方案来手动完成.


你能描述一下你所做的改变吗?为什么?经过一整天的编码,我的眼球太累了,无法自己尝试对其进行逆向工程:)
尝试再次复制和粘贴如何 - 就像 99% 的标签一样搞砸了。
@Charlie Flowers,@ZXX 将文本编辑为描述无法使 xml 布局良好。
从 Microsoft.Common.targets:GetCopyToOutputDirectoryItems 获取可能需要传输到输出目录的所有项目项。这包括来自可传递引用项目的行李项。看起来这个目标计算了所有引用项目的内容项的完全传递闭包;然而事实并非如此。
它仅从其直接子代而不是子代的子代中收集内容项。发生这种情况的原因是 _SplitProjectReferencesByFileExistence 使用的 ProjectReferenceWithConfiguration 列表仅填充在当前项目中,而在子项目中为空。空列表导致 _MSBuildProjectReferenceExistent 为空并终止递归。所以它似乎没有用。
P
Pat

我建议您阅读 Patric Smacchia 关于该主题的文章:

通过 .NET 程序集和 Visual Studio 项目对代码库进行分区 --> 每个 Visual Studio 项目真的应该在自己的程序集中吗? 'Copy Local=True' 的真正含义是什么?

从 NUnit 代码库中吸取的教训 --> VisualStudio 项目参考 + 复制本地 true 选项是邪恶的!)

分析 CruiseControl.NET 的代码库 --> 错误使用 Copy Local Reference Assembly 选项设置为 True)

CC.Net VS 项目依赖于设置为 true 的复制本地引用程序集选项。 [...] 这不仅显着增加了编译时间(在 NUnit 的情况下为 x3),而且还扰乱了您的工作环境。最后但同样重要的是,这样做会带来版本控制潜在问题的风险。顺便说一句,如果 NDepend 在 2 个具有相同名称但内容或版本不同的不同目录中找到 2 个程序集,它将发出警告。正确的做法是定义 2 个目录 $RootDir$\bin\Debug 和 $RootDir$\bin\Release,并将您的 VisualStudio 项目配置为在这些目录中发出程序集。所有项目引用都应引用 Debug 目录中的程序集。

您还可以阅读 this article 以帮助您减少项目数量并缩短编译时间。


我希望我能以不止一个赞成票来推荐 Smacchia 的做法!减少项目数量是关键,而不是拆分解决方案。
t
tRuEsAtM

我建议对几乎所有项目都使用 copy local = false ,除了依赖关系树顶部的项目。对于顶部集合中的所有引用,请复制 local = true。我看到很多人建议共享一个输出目录;我认为这是一个基于经验的可怕想法。如果您的启动项目包含对任何其他项目包含对您的引用的 dll 的引用,即使在所有内容上都复制本地 = false 并且您的构建将失败,有时也会遇到访问\共享冲突。这个问题很烦人,很难追查。我完全建议远离分片输出目录,而不是让项目位于依赖链的顶部,将所需的程序集写入相应的文件夹。如果您在“顶部”没有项目,那么我建议您使用构建后的副本将所有内容放在正确的位置。另外,我会尽量记住调试的便利性。我仍然保留副本 local=true 的任何 exe 项目,因此 F5 调试体验将起作用。


我有同样的想法,并希望在这里找到其他想法相同的人;但是,我很好奇为什么这篇文章没有更多的赞成票。不同意的人:你为什么不同意?
不,除非同一个项目构建两次,否则这不会发生,如果它构建一次并且不复制任何文件,为什么它会被覆盖\访问\共享冲突?
这个。如果开发工作流程需要构建 sln 的一个项目,而解决方案的另一个可执行项目正在运行,那么将所有内容都放在同一个输出目录中将是一团糟。在这种情况下,分离可执行输出文件夹要好得多。
C
Community

你是对的。 CopyLocal 绝对会扼杀您的构建时间。如果你有一个大的源代码树,那么你应该禁用 CopyLocal。不幸的是,干净地禁用它并不容易。我已经回答了关于在 How do I override CopyLocal (Private) setting for references in .NET from MSBUILD 禁用 CopyLocal 的确切问题。一探究竟。以及Best practices for large solutions in Visual Studio (2008).

这是我看到的有关 CopyLocal 的更多信息。

CopyLocal 的实现是为了支持本地调试。当您准备应用程序以进行打包和部署时,您应该将项目构建到相同的输出文件夹中,并确保那里拥有您需要的所有引用。

我在文章 MSBuild: Best Practices For Creating Reliable Builds, Part 2 中写过如何处理构建大型源代码树。


B
Bruno Shine

在我看来,拥有 100 个项目的解决方案是一个大错误。您可以将解决方案拆分为有效的逻辑小单元,从而简化维护和构建。


布鲁诺,请看我上面的后续问题——如果我们分解成更小的 .sln 文件,你如何管理调试与发布方面,然后将其纳入我的参考资料的提示路径?
我同意这一点,我正在使用的解决方案有大约 100 个项目,其中只有少数有 3 个以上的类,构建时间令人震惊,因此我的前辈将解决方案分成 3 个,这完全打破了“查找所有引用和重构。整个事情可以放在几个项目中,这些项目将在几秒钟内完成!
戴夫,好问题。在我工作的地方,我们构建了脚本,这些脚本可以为给定的解决方案构建依赖项,并将二进制文件放在有问题的解决方案可以获取它们的地方。这些脚本针对调试和发布版本进行了参数化。缺点是需要额外的时间来构建上述脚本,但它们可以跨应用程序重复使用。按照我的标准,这个解决方案运行良好。
M
Matt Slagle

我很惊讶没有人提到使用硬链接。它不是复制文件,而是创建指向原始文件的硬链接。这可以节省磁盘空间并大大加快构建速度。这可以使用以下属性在命令行上启用:

/p:CreateHardLinksForAdditionalFilesIfPossible=true;CreateHardLinksForCopyAdditionalFilesIfPossible=true;CreateHardLinksForCopyFilesToOutputDirectoryIfPossible=true;CreateHardLinksForCopyLocalIfPossible=true;CreateHardLinksForPublishFilesIfPossible=true

您还可以将其添加到中央导入文件中,以便您的所有项目也可以获得此好处。


T
Torbjörn Gyllebring

如果您通过项目引用或解决方案级别的依赖项定义了依赖项结构,则可以安全地打开“复制本地”,我什至会说这是一个最佳实践,因为这将让您使用 MSBuild 3.5 并行运行您的构建(通过 /maxcpucount) 尝试复制引用的程序集时,不同的进程不会相互绊倒。


A
Aidin

我们的“最佳做法”是避免许多项目的解决方案。我们有一个名为“matrix”的目录,其中包含当前版本的程序集,所有引用都来自该目录。如果您更改了某个项目并且可以说“现在更改已完成”,您可以将程序集复制到“矩阵”目录中。因此,依赖于该程序集的所有项目都将具有当前(=最新)版本。

如果解决方案中的项目很少,则构建过程会快得多。

您可以使用 Visual Studio 宏或使用“菜单 -> 工具 -> 外部工具...”自动执行“将程序集复制到矩阵目录”步骤。


S
Sheen

您不需要更改 CopyLocal 值。您需要做的就是为解决方案中的所有项目预定义一个通用 $(OutputPath) 并将 $(UseCommonOutputDirectory) 预设为 true。看到这个:http://blogs.msdn.com/b/kirillosenkov/archive/2015/04/04/using-a-common-intermediate-and-output-directory-for-your-solution.aspx


我不确定这在 2009 年是否可用,但它在 2015 年似乎运行良好。谢谢!
C
Community

设置 CopyLocal=false 将减少构建时间,但在部署期间可能会导致不同的问题。

有很多情况下,当您需要将 Copy Local' 保留为 True 时,例如

顶级项目,

二级依赖,

反射调用的 DLL

 SO 问题
When should copy-local be set to true and when should it not?”、
Error message 'Unable to load one or more of the requested types. Retrieve the LoaderExceptions property for more information.'
和  aaron-stainbackanswer 中描述的可能问题。

我设置 CopyLocal=false 的经验并不成功。请参阅我的博文 "Do NOT Change "Copy Local” project references to false, unless understand subsequences."

解决问题的时间超过了设置 copyLocal=false 的好处。


设置 CopyLocal=False 肯定会导致一些问题,但有解决这些问题的方法。此外,您应该修复博客的格式,它几乎不可读,并且在那里说“我被来自<随机公司>的<随机顾问>警告关于部署期间可能出现的错误”不是一个论据。你需要发展。
@GeorgesDupéron,解决问题的时间超过了设置 copyLocal=false 的好处。提到顾问不是争论,而是信用,我的博客解释了问题所在。感谢您的反馈。格式化,我会修复它。
k
kenny

我倾向于构建到一个公共目录(例如 ..\bin),所以我可以创建小型测试解决方案。


A
Aleksandar

您可以尝试使用将复制项目之间共享的所有程序集的文件夹,然后创建一个 DEVPATH 环境变量并在每个开发人员的 machine.config 文件中设置

<developmentMode developerInstallation="true" />

工作站。您唯一需要做的就是复制 DEVPATH 变量指向的文件夹中的任何新版本。

如果可能,还将您的解决方案分成几个较小的解决方案。


有趣...这将如何与调试版本和发布版本一起使用?
我不确定是否存在任何适合通过 DEVPATH 加载调试/发布程序集的解决方案,它仅用于共享程序集,我不建议将它用于进行常规构建。另请注意,在使用此技术时,程序集版本和 GAC 会被覆盖。
j
jyoung

这可能不是最佳实践,但这就是我的工作方式。

我注意到托管 C++ 将其所有二进制文件转储到 $(SolutionDir)/'DebugOrRelease' 中。所以我也把我所有的 C# 项目都放在那里了。我还关闭了解决方案中对项目的所有引用的“复制本地”。在我的小型 10 项目解决方案中,我的构建时间得到了显着改善。此解决方案混合了 C#、托管 C++、本机 C++、C# Web 服务和安装程序项目。

也许有些东西坏了,但由于这是我工作的唯一方式,我没有注意到它。

找出我正在破坏的东西会很有趣。


C
CubanX

通常,如果您希望项目使用 Bin 中的 DLL 而不是其他地方(GAC、其他项目等)的 DLL,则只需要复制本地

我倾向于同意其他人的观点,即如果可能的话,您也应该尝试打破该解决方案。

您还可以使用配置管理器在一个仅构建给定项目集的解决方案中为自己设置不同的构建配置。

如果所有 100 个项目都相互依赖,这似乎很奇怪,因此您应该能够将其拆分或使用 Configuration Manager 来帮助自己。


B
Bruno Shine

您可以让您的项目引用指向 dll 的调试版本。与您的 msbuild 脚本相比,您可以设置 /p:Configuration=Release,因此您将拥有应用程序的发布版本和所有附属程序集。


Bruno - 是的,这适用于 Project References,这也是我们最初选择 100 个项目解决方案的原因之一。它不适用于我浏览到预构建的调试版本的引用 - 我最终得到一个针对调试程序集构建的发布应用程序,这是一个问题
在文本编辑器中编辑项目文件并在 HintPath 中使用 $(Configuration),例如 ..\output\$(Configuration)\test.dll
u
user306031

如果您想在没有 GAC 的情况下使用 copy local false 来引用 DLL 的中心位置,除非您这样做。

http://nbaked.wordpress.com/2010/03/28/gac-alternative/


S
SharpGobi

如果引用不包含在 GAC 中,我们必须将 Copy Local 设置为 true 以便应用程序能够正常工作,如果我们确定引用将预安装在 GAC 中,则可以将其设置为 false。


佚名

好吧,我当然不知道问题是如何解决的,但是我接触了一个构建解决方案,它可以帮助自己,这样所有创建的文件都可以在符号链接的帮助下放在 ramdisk 上。

c:\solution 文件夹\bin -> ramdisk r:\solution 文件夹\bin\

c:\solution 文件夹\obj -> ramdisk r:\solution 文件夹\obj\

您还可以另外告诉视觉工作室它可以用于构建的临时目录。

事实上,它所做的还不止这些。但这确实触动了我对性能的理解。 3 分钟内 100% 的处理器使用和一个包含所有依赖项的大型项目。