﻿# 使用 MSBuild 建置方案檔(sln)與建置專案檔(csproj)的陷阱與注意事項

我一年大概都會幫幾家企業導入 Azure DevOps Server 平台，最近幫客戶導入的過程遇到了一個難題。一個方案檔中有 9 個專案，其中有 4 個 .NET Framework 4.7.2 類別庫專案、1 個 .NET Framework 4.7.2 的 ASP.NET Web Forms 專案、2 個 .NET Core 2.1 類別庫專案、1 個 .NET Standard 2.0 專案、2 個 .NET Core 2.1 類別庫專案、1 個 .NET Core 2.1 的 ASP.NET Core 專案。很少看到一個案子用這麼混搭的技術，而這個案子要做 CI/CD 確實也遇到了問題。這篇文章我將說明問題與解決方法！

### 問題說明 (1)

導入 CI/CD 的過程，首先要先確認透過 Visual Studio 可否順利建置，是可以的！

基本上只要可以順利透過 Visual Studio 2019 建置，那麼我們透過 MSBuild 一定也可以順利建置，只是這當中有很多不為人知的秘密，埋藏在一堆 Visual Studio 2019 才有的 `*.targets` 目標檔，不去分析 MSBuild 載入的檔案，很難抓到問題的根源。我這次導入這個方案的過程，花了我不少時間研究問題發生的原因，快速解決問題的方法不是沒有，但是不能釐清真相，就是讓人有那麼一絲絲的不太放心。

我習慣用 .NET CLI 來建置 .NET Core 專案，但是這個專案用 `dotnet build` 就是無法建置，因為在方案檔中用了一些 Visual Studio 2019 才有的目標檔 (`*.targets`)，在 .NET SDK 中找不到，所以被迫要用 MSBuild 來建置專案，但過程並不順利。

首先，在 Azure DevOps Server 預設安裝完成後，設定 Pipelines 的時候 **MSBuild 架構**提供了兩個版本：

1.  `MSBuild x86` (預設值)
    
    ```batch
    C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\msbuild.exe
    ```
    
2.  `MSBuild x64`
    
    ```batch
    C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\amd64\msbuild.exe
    ```
    

基本上，這兩個 MSBuild 架構用來建置 .NET Framework 4.7.2 並沒有問題，但是用來建置 .NET Standard 2.0 類別庫專案的時候卻會失敗。要是用 .NET CLI 的 `dotnet build` 來建置，問題老早就解決了，可惜這個專案卻不能用，只能透過 MSBuild 才行。

如果我用預設的 `MSBuild x86` 來**建置整個方案**，就會遇到以下問題：

```log
MSBuild MyProject.sln /p:platform="any cpu" /p:configuration="release"
```

```log
##[error]MyProject.ViewModels\obj\Release\netstandard2.0\.NETStandard,Version=v2.0.AssemblyAttributes.cs(4,20): Error CS0400: 全域命名空間中找不到類型或命名空間名稱 'System' (是否遺漏了組件參考?)
```

嘗試了很久，最後發現有一個簡單的解決方法，那就是改用 `MSBuild x64` 就解決了，真是花惹發！

### 問題說明 (2)

好的，先解決**建置整個方案**的難題，問題就已經克服了一半，接著就是要分別建置方案中的兩個主要專案，一個是 **.NET Framework 4.7.2** 的 **ASP.NET Web Forms** 專案，另一個是 **.NET Core 2.1** 的 **ASP.NET Core** 專案。

因為兩個不同架構的東西，我就先嘗試分別建置**專案檔**(`*.csproj`)，結果遇到以下問題：

1.  **.NET Core 2.1** 的 **ASP.NET Core** 專案
    
    ```log
    MSBuild MyProject\MyProject.csproj /p:platform="any cpu" /p:configuration="release"
    ```
    
    建置成功！
    
2.  **.NET Framework 4.7.2** 的 **ASP.NET Web Forms** 專案
    
    ```log
    MSBuild MyProject.BackEnd\MyProject.BackEnd.csproj /p:platform="any cpu" /p:configuration="release"
    ```
    
    **建置失敗**！錯誤訊息如下：
    
    ```log
    ##[error]C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\amd64\Microsoft.Common.CurrentVersion.targets(820,5): Error : The BaseOutputPath/OutputPath property is not set for project 'MyProject.BackEnd.csproj'.  Please check to make sure that you have specified a valid combination of Configuration and Platform for this project.  Configuration='release'  Platform='any cpu'.  You may be seeing this message because you are trying to build a project without a solution file, and have specified a non-default Configuration or Platform that doesn't exist for this project.
    ```
    

進一步研究 `MyProject.BackEnd.csproj` 後發現，Azure DevOps 上面的 `$(BuildPlatform)` 變數預設是 `any cpu`，但我從 `MyProject.BackEnd.csproj` 裡面含有 `OutputPath` 屬性的地方，有個 `PropertyGroup` 的 `Condition` 是 `'$(Configuration)|$(Platform)' == 'Release|AnyCPU'`，這裡的 `$(Platform)` 應該是 `AnyCPU` 才對：

```xml
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
  <DebugSymbols>true</DebugSymbols>
  <DebugType>pdbonly</DebugType>
  <Optimize>true</Optimize>
  <OutputPath>bin\</OutputPath>
  <DefineConstants>TRACE</DefineConstants>
  <ErrorReport>prompt</ErrorReport>
  <WarningLevel>4</WarningLevel>
</PropertyGroup>
```

詭異的地方就在於，我明明在 Visual Studio 2019 中都可以順利建置，用 MSBuild 也可以順利建置 `MyProject.sln` 方案檔，但直接建置 `MyProject.BackEnd.csproj` 就不行呢？可是建置 .NET Core 的專案檔裡面，也有相關的 `PropertyGroup`，也有設定一樣的 `Condition` 就沒事呢？

```xml
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
  <DocumentationFile>bin\Release\netcoreapp2.1\MyProject.xml</DocumentationFile>
</PropertyGroup>
```

我進一步看了 `MyProject.sln` 方案檔的內容，發現在方案檔中的 `Platform` 定義，的確是 `Any CPU` 沒錯：

```
GlobalSection(SolutionConfigurationPlatforms) = preSolution
    Debug|Any CPU = Debug|Any CPU
    Release|Any CPU = Release|Any CPU
EndGlobalSection
```

目前到這裡，就是一堆無法理解的問題了，而且兩邊有點衝突，這個詭異的差異 (`Any CPU` v.s. `AnyCPU`)，讓我在建置專案檔的時候，必須指定不同的 `$(BuildPlatform)` 變數值才能解決。所以我的第一版解決方法是：

1.  **.NET Core 2.1** 的 **ASP.NET Core** 專案
    
    ```log
    MSBuild MyProject\MyProject.csproj /p:platform="anycpu" /p:configuration="release"
    ```
    
    建置成功！
    
2.  **.NET Framework 4.7.2** 的 **ASP.NET Web Forms** 專案
    
    ```log
    MSBuild MyProject.BackEnd\MyProject.BackEnd.csproj /p:platform="anycpu" /p:configuration="release"
    ```
    
    建置成功！
    

### 比較漂亮的解決方法

雖然上述的作法是可以成功建置的，但我無法接受，一定有個更合理的解決方法才對。

進一步研究後發現，原來用 MSBuild 建置 `*.sln` 方案檔的時候，事實上會在背後偷偷幫你產生一個暫時的 `*.csproj` 專案檔 (MSBuild File)，並且幫你建置專案。所以上述的問題，我發現有個更漂亮的解決方式，那就是直接建置 `*.sln` 方案檔，但指定特定專案進行建置！

1.  **.NET Core 2.1** 的 **ASP.NET Core** 專案
    
    ```batch
    MSBuild MyProject.sln /p:platform="any cpu" /p:configuration="release" /t:MyProject
    ```
    
    建置成功！
    
2.  **.NET Framework 4.7.2** 的 **ASP.NET Web Forms** 專案
    
    ```batch
    MSBuild MyProject.sln /p:platform="any cpu" /p:configuration="release" /t:MyProject.BackEnd
    ```
    
    建置失敗，因為方案檔的**目標**(`/t:XXX`)不能包含特殊符號，所有符號都要換成「底線」才能執行成功，所以正確的命令應該要改成這樣：
    
    ```batch
    MSBuild MyProject.sln /p:platform="any cpu" /p:configuration="release" /t:MyProject_BackEnd
    ```
    

事實上，上面這種解決方案才是透過 MSBuild 建置專案時的標準方法，尤其是在實作 CI/CD 的時候，就應該這麼做，才能盡可能的貼近你在 Visual Studio 2019 建置專案的行為。如果我們要使用 Visual Studio publish profiles (`.pubxml`) 來發行應用程式，我們可以不用針對專案檔進行操作，直接用這個技巧就可以，例如：

```batch
MSBuild MyProject.sln /p:platform="any cpu" /p:configuration="release" /t:MyProject_BackEnd /p:DeployOnBuild=true /p:PublishProfile="FolderProfile" /p:publishUrl="$(build.artifactstagingdirectory)\MyProject.BackEnd\\"
```

> 記得: `/p:publishUrl="..."` 的最後面如果是 `\` 結尾，一定要改成兩個反斜線 (`\\`) 才是正確語法！

### 直接執行 MSBuild 命令的技巧

有時候我們想直接透過命令列直接執行 MSBuild 來建置或發行專案，但是偶爾會遇到一些**環境變數**的問題，這邊我分享一個很簡單的解決方案。

你可以在執行 MSBuild 命令之前先執行以下命令，就可以載入 `Developer Command Prompt for VS 2019` 所需要載入的所有設定，讓你在執行的時候減少很多潛在的問題！

-   Visual Studio 2019 Community Edition
    
    ```batch
    CALL "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\Tools\VsDevCmd.bat"
    ```
    
-   Visual Studio 2019 Enterprise Edition
    
    ```batch
    CALL "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\Tools\VsDevCmd.bat"
    ```
    

### 相關連結

-   [build - specify project file of a solution using msbuild - Stack Overflow](https://stackoverflow.com/a/19534376/910074)
-   CI Build doesn't include references in MSBuild CoreCompile commands
-   [Visual Studio publish profiles (.pubxml) for ASP.NET Core app deployment](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/visual-studio-publish-profiles?view=aspnetcore-5.0&WT.mc_id=DT-MVP-4015686)
-   [tfs - Microsoft.VisualStudio.SharePoint.targets (416): Could not load file or assembly 'Microsoft.VisualStudio.SharePoint.Designers.Model - Stack Overflow](https://stackoverflow.com/a/28416625/910074)
