解决 ASP.NET Web 项目在 CI/CD 环境下 MSBuild 发布丢失 Roslyn 文件的问题
问题背景
在将持续集成与持续部署(CI/CD)流程应用于基于 .NET Framework 4.8.1 的传统 ASP.NET Web 应用程序时,可能会遇到一个隐蔽的构建问题:通过 GitLab Runner 调用 MSBuild 进行发布后,最终的输出目录中缺失 bin\roslyn 文件夹。这会导致依赖 Roslyn 编译器平台的动态编译功能(如 Razor 视图动态编译)在运行时失效。本文将深入剖析该问题的成因并提供可靠的解决方案。
Roslyn 在传统 ASP.NET 项目中的集成方式
要理解为何会丢失该文件夹,首先需要明确 Roslyn 编译器在旧版 Web 项目中的存在形式与配置方式。
1. 配置文件中的编译器声明
在项目的 web.config 文件末尾,通常包含 system.codedom 节点,用于指定使用 Roslyn 提供程序来处理 C# 和 VB.NET 代码的运行时编译:
<configuration>
<!-- 其他系统配置节点 -->
<system.codedom>
<compilers>
<compiler language="c#;cs;csharp" extension=".cs"
type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.CSharpCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=4.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
warningLevel="4" compilerOptions="/langversion:default /nowarn:1659;1699;1701" />
<compiler language="vb;vbs;visualbasic;vbscript" extension=".vb"
type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.VBCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=4.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
warningLevel="4" compilerOptions="/langversion:default /nowarn:41008 /define:_MYTYPE="Web" /optionInfer+" />
</compilers>
</system.codedom>
</configuration>
2. NuGet 包依赖
项目文件(.csproj)中必须引入相关的 NuGet 包,以提供底层的编译工具链和运行时程序集:
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="Microsoft.CodeDom.Providers.DotNetCompilerPlatform" Version="4.1.0" />
<PackageReference Include="Microsoft.Net.Compilers" Version="4.2.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>
这些包不仅提供了运行时的 DLL,还包含了 csc.exe 和 vbc.exe 等实际执行编译任务的工具,它们最终需要被部署到站点的 bin\roslyn 目录下。
问题排查与日志分析
为了定位文件丢失的原因,可以在 CI/CD 脚本中为 MSBuild 启用诊断级别的日志输出(添加 /v:diag 参数)。由于诊断日志体积庞大,可能需要临时调高 GitLab Runner 的日志大小限制(例如调整至 8MB 以上)。
通过分析构建日志中关于 CopyRoslynCompilerFilesToOutputDirectory 目标的执行记录,可以发现以下关键信息:
目标 "CopyRoslynCompilerFilesToOutputDirectory":
任务 "Copy"
参数: DestinationFolder = D:\agent\work\src\MyWebApp\bin\roslyn
参数: SourceFiles = C:\Users\builder\.nuget\packages\microsoft.codedom.providers.dotnetcompilerplatform\4.1.0\tools\roslyn-4.1.0\csc.exe ...
正在创建目录 "D:\agent\work\src\MyWebApp\bin\roslyn"。
日志揭示了问题的核心矛盾:
- 路径参数未生效:尽管在调用 MSBuild 时通过
/p:WebProjectOutputDir=D:\publish\output指定了自定义的发布目录,但 Roslyn 复制任务并没有使用这个外部传入的路径。 - 文件被复制到了源码目录:Roslyn 工具文件确实被复制了,但目标位置是项目源码树内部的默认
bin\roslyn目录,而不是 CI/CD 管道指定的最终发布目录。当后续阶段打包或部署发布目录时,自然会找不到这些文件。
解决方案:自定义 MSBuild 目标
既然内置的复制任务无法正确识别外部传入的 WebProjectOutputDir 参数,我们可以通过在 .csproj 文件中注入自定义的 MSBuild Target 来手动修正这一行为。
将以下代码片段添加到项目文件的末尾:
<Target Name="DeployRoslynCompilerAssets" AfterTargets="_CopyWebApplication">
<!-- 定义需要复制的 Roslyn 工具文件集合,支持递归子目录 -->
<ItemGroup>
<CompilerAssets Include="$(CscToolPath)\**\*" />
</ItemGroup>
<!-- 计算目标发布目录下的 roslyn 文件夹路径 -->
<PropertyGroup>
<PublishRoslynPath>$(WebProjectOutputDir)\bin\roslyn</PublishRoslynPath>
</PropertyGroup>
<!-- 确保目标目录存在 -->
<MakeDir Directories="$(PublishRoslynPath)" Condition="!Exists('$(PublishRoslynPath)')" />
<!-- 执行复制操作,使用 DestinationFiles 保留目录结构,并加入重试机制 -->
<Copy SourceFiles="@(CompilerAssets)"
DestinationFiles="@(CompilerAssets->'$(PublishRoslynPath)\%(RecursiveDir)%(Filename)%(Extension)')"
SkipUnchangedFiles="true"
OverwriteReadOnlyFiles="true"
Retries="3"
RetryDelayMilliseconds="1000" />
</Target>
配置要点解析
- 执行时机 (
AfterTargets):将目标绑定在_CopyWebApplication之后执行至关重要。在这个内置目标完成后,MSBuild 已经正确解析并应用了命令行传入的WebProjectOutputDir参数,确保自定义任务能获取到准确的发布路径。 - 文件映射 (
DestinationFiles):使用%(RecursiveDir)元数据可以完美保留源文件中的子目录结构,防止嵌套文件在复制时被展平。 - 容错处理:加入
OverwriteReadOnlyFiles和重试参数,可以有效避免在 CI 环境中因文件只读或短暂占用导致的构建中断。