教學課程:建立程式碼產生的自訂工作

在本教學課程中,您將在 C# 的 MSBuild 中建立處理程式碼產生的自訂工作,然後在建置中使用該工作。 此範例示範如何使用 MSBuild 來處理清除和重建作業。 此範例也會顯示如何支援累加建置,因此只有在輸入檔案變更時才會產生程式碼。 示範的技術適用於各種程式碼產生案例。 這些步驟還會顯示如何使用 NuGet 來封裝工作以進行散發,而本教學課程包含一個選擇性步驟,以使用 BinLog 檢視器來改善疑難排解體驗。

必要條件

您應該了解 MSBuild 概念,例如工作、目標和屬性。 請參閱 MSBuild 概念

這些範例需要隨 Visual Studio 一起安裝,但也可以單獨安裝的 MSBuild。 請參閱下載不包含 Visual Studio 的 MSBuild

程式碼範例簡介

此範例採用包含要設定的值的輸入文字檔,並使用建立這些值的程式碼建立 C# 程式碼檔案。 雖然這是簡單的範例,但相同的基本技術可以套用至更複雜的程式碼產生案例。

在本教學課程中,您會建立名為 AppSettingStronglyTyped 的 MSBuild 自訂工作。 該工作會讀取一組文字檔,每個檔案的行格式如下:

propertyName:type:defaultValue

程式碼產生包含所有常數的 C# 類別。 出現問題應該停止組建,並為使用者提供足夠的資訊來診斷問題。

本教學課程的完整範例程式碼位於 GitHub 上 .NET 範例存放庫中的自訂工作 - 程式碼產生

建立 AppSettingStronglyTyped 專案

建立 .NET Standard 類別庫。 架構應該是 .NET Standard 2.0。

請注意完整的 MSBuild (Visual Studio 所使用的 MSBuild) 和可攜式 MSBuild (.NET Core 命令列中的搭售方案) 之間的差異。

  • 完整 MSBuild:這個版本的 MSBuild 通常位於 Visual Studio 內。 在 .NET Framework 上執行。 當您在解決方案或專案上執行建置時,Visual Studio 會使用此專案。 此版本也可從命令列環境中取得,例如 Visual Studio 開發人員命令提示字元或 PowerShell。
  • .NET MSBuild:這個版本的 MSBuild 是 .NET Core 命令列中的搭售方案。 它會在 .NET Core 上執行。 Visual Studio 不會直接叫用此版本的 MSBuild。 它僅支援使用 Microsoft.NET.Sdk 建置的專案。

如果您想要在 .NET Framework 與任何其他 .NET 實作之間的共用程式碼 (例如 .NET Core),您的程式庫應該以 .NET Standard 2.0 為目標,並且您想要在 .NET Framework 上執行的 Visual Studio 內執行。 .NET Framework 不支援 .NET Standard 2.1。

建立 AppSettingStronglyTyped MSBuild 自訂工作

第一個步驟是建立 MSBuild 自訂工作。 如何撰寫 MSBuild 自訂工作的相關資訊,可能有助於了解下列步驟。 MSBuild 自訂工作是實作 ITask 介面的類別。

  1. 新增對 Microsoft.Build.Utilities.Core NuGet 套件的參考,然後建立衍生自 Microsoft.Build.Utilities.Task 的名為 AppSettingStronglyTyped 的類別。

  2. 新增三個屬性。 這些屬性定義使用者在用戶端專案中使用工作時所設定的工作參數:

     //The name of the class which is going to be generated
     [Required]
     public string SettingClassName { get; set; }
    
     //The name of the namespace where the class is going to be generated
     [Required]
     public string SettingNamespaceName { get; set; }
    
     //List of files which we need to read with the defined format: 'propertyName:type:defaultValue' per line
     [Required]
     public ITaskItem[] SettingFiles { get; set; }
    

    該工作會處理 SettingFiles 並產生一個類別 SettingNamespaceName.SettingClassName。 產生的類別會具有根據文字檔內容的一組常數。

    工作輸出應該是一個字串,提供產生的程式碼檔案名稱:

     // The filename where the class was generated
     [Output]
     public string ClassNameFile { get; set; }
    
  3. 當您建立自訂工作時,您會繼承自 Microsoft.Build.Utilities.Task。 若要實作工作,請覆寫 Execute() 方法。 如果工作成功,則 Execute 方法會傳回 true,否則會傳回 falseTask 實作 Microsoft.Build.Framework.ITask 並提供某些 ITask 成員的預設實作,此外也提供一些記錄功能。 要將狀態輸出到記錄檔以診斷工作並對其進行疑難排解非常重要,尤其是發生問題且工作必須傳回錯誤結果時 (false)。 發生錯誤時,該類別會透過呼叫 TaskLoggingHelper.LogError 來發出錯誤訊號。

     public override bool Execute()
     {
     	//Read the input files and return a IDictionary<string, object> with the properties to be created. 
     	//Any format error it will return false and log an error
     	var (success, settings) = ReadProjectSettingFiles();
     	if (!success)
     	{
     			return !Log.HasLoggedErrors;
     	}
     	//Create the class based on the Dictionary
     	success = CreateSettingClass(settings);
    
     	return !Log.HasLoggedErrors;
     }
    

    工作 API 允許傳回 false,指出失敗,而不向使用者指出發生什麼問題。 最好傳回 !Log.HasLoggedErrors 而不是布林程式碼,並在發生問題時記錄錯誤。

記錄錯誤

記錄錯誤時的最佳做法是在記錄錯誤時提供詳細資料,例如行號和不同的錯誤碼。 下列程式碼會剖析文字輸入檔,並使用 TaskLoggingHelper.LogError 方法搭配產生錯誤的文字檔中的行號。

private (bool, IDictionary<string, object>) ReadProjectSettingFiles()
{
	var values = new Dictionary<string, object>();
	foreach (var item in SettingFiles)
	{
		int lineNumber = 0;

		var settingFile = item.GetMetadata("FullPath");
		foreach (string line in File.ReadLines(settingFile))
		{
			lineNumber++;

			var lineParse = line.Split(':');
			if (lineParse.Length != 3)
			{
				Log.LogError(subcategory: null,
							 errorCode: "APPS0001",
							 helpKeyword: null,
							 file: settingFile,
							 lineNumber: lineNumber,
							 columnNumber: 0,
							 endLineNumber: 0,
							 endColumnNumber: 0,
							 message: "Incorrect line format. Valid format prop:type:defaultvalue");
							 return (false, null);
			}
			var value = GetValue(lineParse[1], lineParse[2]);
			if (!value.Item1)
			{
				return (value.Item1, null);
			}

			values[lineParse[0]] = value.Item2;
		}
	}
	return (true, values);
}

使用之前的程式碼所顯示的技術,文字輸入檔的語法錯誤會顯示為建置錯誤,並提供實用的診斷資訊:

Microsoft (R) Build Engine version 17.2.0 for .NET Framework
Copyright (C) Microsoft Corporation. All rights reserved.

Build started 2/16/2022 10:23:24 AM.
Project "S:\work\msbuild-examples\custom-task-code-generation\AppSettingStronglyTyped\AppSettingStronglyTyped.Test\bin\Debug\net6.0\Resources\testscript-fail.msbuild" on node 1 (default targets).
S:\work\msbuild-examples\custom-task-code-generation\AppSettingStronglyTyped\AppSettingStronglyTyped.Test\bin\Debug\net6.0\Resources\error-prop.setting(1): error APPS0001: Incorrect line format. Valid format prop:type:defaultvalue [S:\work\msbuild-examples\custom-task-code-generation\AppSettingStronglyTyped\AppSettingStronglyTyped.Test\bin\Debug\net6.0\Resources\testscript-fail.msbuild]
Done Building Project "S:\work\msbuild-examples\custom-task-code-generation\AppSettingStronglyTyped\AppSettingStronglyTyped.Test\bin\Debug\net6.0\Resources\testscript-fail.msbuild" (default targets) -- FAILED.

Build FAILED.

"S:\work\msbuild-examples\custom-task-code-generation\AppSettingStronglyTyped\AppSettingStronglyTyped.Test\bin\Debug\net6.0\Resources\testscript-fail.msbuild" (default target) (1) ->
(generateSettingClass target) ->
  S:\work\msbuild-examples\custom-task-code-generation\AppSettingStronglyTyped\AppSettingStronglyTyped.Test\bin\Debug\net6.0\Resources\error-prop.setting(1): error APPS0001: Incorrect line format. Valid format prop:type:defaultvalue [S:\work\msbuild-examples\custom-task-code-generation\AppSettingStronglyTyped\AppSettingStronglyTyped.Test\bin\Debug\net6.0\Resources\testscript-fail.msbuild]

	 0 Warning(s)
	 1 Error(s)

當您在工作中攔截例外狀況時,請使用 TaskLoggingHelper.LogErrorFromException 方法。 這會改善錯誤輸出,例如,透過取得擲回例外狀況的呼叫堆疊。

catch (Exception ex)
{
	// This logging helper method is designed to capture and display information
	// from arbitrary exceptions in a standard way.
	Log.LogErrorFromException(ex, showStackTrace: true);
	return false;
}

此處不會顯示使用這些輸入來建置所產生程式碼檔案文字的其他方法實作;請參閱範例存放庫中的 AppSettingStronglyTyped.cs

範例程式碼會在建置流程期間產生 C# 程式碼。 該工作與任何其他 C# 類別一樣,因此當您完成本教學課程時,可以對其進行自訂,並新增自己的案例所需的任何功能。

產生主控台應用程式並使用自訂工作

在本節中,您將建立一個使用該工作的標準 .NET Core 主控台應用程式。

重要

請務必避免在即將使用的相同 MSBuild 流程中產生 MSBuild 自訂工作。 新專案應該位於完全不同的 Visual Studio 解決方案中,或新專案使用從標準輸出預先產生並重新配置的 dll。

  1. 在新的 Visual Studio 解決方案中建立 .NET 主控台專案 MSBuildConsoleExample。

    散發工作的一般方式是透過 NuGet 套件,但在開發和偵錯期間,您可以將 .props.targets 的所有資訊直接包含在應用程式的專案檔中,然後在將工作散發給其他人時移至 NuGet 格式。

  2. 修改專案檔以取用程式碼產生工作。 本節中的程式碼清單會顯示參考工作、設定工作的輸入參數,以及撰寫處理清除和重建作業的目標之後修改的專案檔,以便如預期般移除產生的程式碼檔案。

    工作是使用 UsingTask 元素 (MSBuild) 註冊的。 UsingTask 元素會註冊工作;它會告訴 MSBuild 工作的名稱,以及如何尋找並執行包含工作類別的組件。 組件路徑相對於專案檔。

    PropertyGroup 包含對應至工作中定義的屬性的屬性定義。 這些屬性是使用屬性來設定,而工作名稱會作為元素名稱使用。

    TaskName 是要從組件中參考的工作名稱。 此屬性應該一律使用完整指定的命名空間。 AssemblyFile 是組件的檔案路徑。

    若要叫用工作,請將工作新增至適當的目標,在此案例 GenerateSetting 中。

    目標 ForceGenerateOnRebuild 會藉由刪除產生的檔案來處理清除和重建作業。 透過將 AfterTargets 屬性設定為 CoreClean,將其設定為在 CoreClean 目標之後執行。

     <Project Sdk="Microsoft.NET.Sdk">
     	<UsingTask TaskName="AppSettingStronglyTyped.AppSettingStronglyTyped" AssemblyFile="..\..\AppSettingStronglyTyped\AppSettingStronglyTyped\bin\Debug\netstandard2.0\AppSettingStronglyTyped.dll"/>
    
     	<PropertyGroup>
     		<OutputType>Exe</OutputType>
     		<TargetFramework>net6.0</TargetFramework>
     		<RootFolder>$(MSBuildProjectDirectory)</RootFolder>
     		<SettingClass>MySetting</SettingClass>
     		<SettingNamespace>MSBuildConsoleExample</SettingNamespace>
     		<SettingExtensionFile>mysettings</SettingExtensionFile>
     	</PropertyGroup>
    
     	<ItemGroup>
     		<SettingFiles Include="$(RootFolder)\*.mysettings" />
     	</ItemGroup>
    
     	<Target Name="GenerateSetting" BeforeTargets="CoreCompile" Inputs="@(SettingFiles)" Outputs="$(RootFolder)\$(SettingClass).generated.cs">
     		<AppSettingStronglyTyped SettingClassName="$(SettingClass)" SettingNamespaceName="$(SettingNamespace)" SettingFiles="@(SettingFiles)">
     		<Output TaskParameter="ClassNameFile" PropertyName="SettingClassFileName" />
     		</AppSettingStronglyTyped>
     		<ItemGroup>
     			<Compile Remove="$(SettingClassFileName)" />
     			<Compile Include="$(SettingClassFileName)" />
     		</ItemGroup>
     	</Target>
    
     	<Target Name="ForceReGenerateOnRebuild" AfterTargets="CoreClean">
     		<Delete Files="$(RootFolder)\$(SettingClass).generated.cs" />
     	</Target>
     </Project>
    

    注意

    此程式碼沒有覆寫 CoreClean 等目標,而是使用另一種方式來排序目標 (BeforeTarget 和 AfterTarget)。 SDK 樣式專案在專案檔的最後一行之後隱含匯入目標;這表示除非手動指定匯入,否則無法覆寫預設目標。 請參閱覆寫預先定義的目標

    InputsOutputs 屬性可藉由提供累加建置的資訊,協助 MSBuild 更有效率。 輸入的日期會與輸出日期進行比較,以查看是否需要執行目標,或是否可以重複使用上一個組建的輸出。

  3. 建立輸入文字檔,具有定義要探索的副檔名。 使用預設延伸模組,在根目錄上建立 MyValues.mysettings,其中包含下列內容:

     Greeting:string:Hello World!
    
  4. 再次建置,而會建立產生的檔案並加以建置。 檢查 MySetting.generated.cs 檔案的專案資料夾。

  5. MySetting 類別位於錯誤的命名空間中,因此現在請進行變更以使用我們的應用程式命名空間。 開啟專案檔並新增下列程式碼:

     <PropertyGroup>
     	<SettingNamespace>MSBuildConsoleExample</SettingNamespace>
     </PropertyGroup>
    
  6. 再次重建,並觀察該類別位於 MSBuildConsoleExample 命名空間中。 如此一來,您可以重新定義產生的類別名稱 (SettingClass)、要用作輸入的文字擴充檔 (SettingExtensionFile),以它們的位置 (RootFolder)。

  7. 開啟 Program.cs 並變更硬式編碼「Hello World!!」 至使用者定義的常數:

     static void Main(string[] args)
     {
     	Console.WriteLine(MySetting.Greeting);
     }
    

執行程式;它會從產生的類別列印問候語。

(選擇性) 在建置流程期間記錄事件

您可以使用命令列命令進行編譯。 瀏覽至專案資料夾。 您會使用 -bl (二進位記錄檔) 選項來產生二進位記錄檔。 二進位記錄檔將包含有用的資訊,以了解建置流程期間發生的情況。

# Using dotnet MSBuild (run core environment)
dotnet build -bl

# or full MSBuild (run on net framework environment; this is used by Visual Studio)
msbuild -bl

這兩個命令都會產生一個記錄檔 msbuild.binlog,可以使用 MSBuild 二進位和結構化記錄檢視器開啟該檔案。 選項 /t:rebuild 表示執行重建目標。 它會強制重新產生產生的程式碼檔案。

恭喜! 您已建置產生程式碼的工作,並在組建中使用。

封裝散發工作

如果您只需要在幾個專案或單一解決方案中使用自訂工作,則將工作作為原始元件使用就符合所需了,但準備工作以在別處使用或與其他人共用工作的最佳方法是作為 NuGet 套件。

MSBuild 工作套件與程式庫 NuGet 套件有一些主要差異:

  • 他們必須與自己的組件相依性組合,而不是將這些相依性公開給使用該組件的專案
  • 它們不會將任何必要的組件封裝到 lib/<target framework> 資料夾,因為這會導致 NuGet 將組件包含在取用工作的任何套件中
  • 它們只需要針對 Microsoft.Build 組件進行編譯 - 在執行階段,這些組件將由實際 MSBuild 引擎提供,因此不需要包含在套件中
  • 他們會產生特殊 .deps.json 檔案,協助 MSBuild 以一致的方式載入工作的相依性 (特別是原生相依性)

若要完成所有這些目標,您必須對標準專案檔進行可能超出您熟悉範圍的一些變更。

建立 NuGet 套件

建立 NuGet 套件是將自訂工作散發給其他人的建議方式。

準備產生套件

若要準備產生 NuGet 套件,請對專案檔進行一些變更,以指定描述套件的詳細資料。 您建立的初始專案檔類似下列程式碼:

<Project Sdk="Microsoft.NET.Sdk">

	<PropertyGroup>
		<TargetFramework>netstandard2.0</TargetFramework>
	</PropertyGroup>

	<ItemGroup>
		<PackageReference Include="Microsoft.Build.Utilities.Core" Version="17.0.0" />
	</ItemGroup>

</Project>

若要產生 NuGet 套件,請新增下列程式碼來設定套件的屬性。 您可以在套件文件中查看支援的 MSBuild 屬性的完整清單:

<PropertyGroup>
	... 
	<IsPackable>true</IsPackable>
	<Version>1.0.0</Version>
	<Title>AppSettingStronglyTyped</Title>
	<Authors>Your author name</Authors>
	<Description>Generates a strongly typed setting class base on a text file.</Description>
	<PackageTags>MyTags</PackageTags>
	<Copyright>Copyright ©Contoso 2022</Copyright>
	...
</PropertyGroup>

將相依性標示為私人

MSBuild 工作的相依性必須封裝在套件內;它們無法表示為一般套件參考。 該套件不會向外部使用者公開任何常規相依性。 這需要兩個步驟來完成:將組件標示為私人,並將它們實際內嵌到產生的套件中。 在此範例中,我們假設您的工作依賴 Microsoft.Extensions.DependencyInjection 才能運作,因此請將 PackageReference 新增至版本 6.0.0Microsoft.Extensions.DependencyInjection 中。

<ItemGroup>
	<PackageReference 
		Include="Microsoft.Build.Utilities.Core"
		Version="17.0.0" />
	<PackageReference
		Include="Microsoft.Extensions.DependencyInjection"
		Version="6.0.0" />
</ItemGroup>

現在,使用 PrivateAssets="all" 屬性來標記此工作項目的每個相依性,包含 PackageReferenceProjectReference。 這會告訴 NuGet 完全不要將這些相依性公開給取用專案。 您可以在 NuGet 文件深入了解控制相依性資產。

<ItemGroup>
	<PackageReference 
		Include="Microsoft.Build.Utilities.Core"
		Version="17.0.0"
		PrivateAssets="all"
	/>
	<PackageReference
		Include="Microsoft.Extensions.DependencyInjection"
		Version="6.0.0"
		PrivateAssets="all"
	/>
</ItemGroup>

將相依性組合至套件

您也必須將相依性的執行階段資產內嵌至工作套件中。 這有兩個部分:一個是將相依性新增至 BuildOutputInPackage ItemGroup 的 MSBuild 目標,以及一些控制這些 BuildOutputInPackage 項目配置的屬性。 您可以在 NuGet 文件中深入了解此流程。

<PropertyGroup>
	...
	<!-- This target will run when MSBuild is collecting the files to be packaged, and we'll implement it below. This property controls the dependency list for this packaging process, so by adding our custom property we hook ourselves into the process in a supported way. -->
	<TargetsForTfmSpecificBuildOutput>
		$(TargetsForTfmSpecificBuildOutput);CopyProjectReferencesToPackage
	</TargetsForTfmSpecificBuildOutput>
	<!-- This property tells MSBuild where the root folder of the package's build assets should be. Because we are not a library package, we should not pack to 'lib'. Instead, we choose 'tasks' by convention. -->
	<BuildOutputTargetFolder>tasks</BuildOutputTargetFolder>
	<!-- NuGet does validation that libraries in a package are exposed as dependencies, but we _explicitly_ do not want that behavior for MSBuild tasks. They are isolated by design. Therefore we ignore this specific warning. -->
	<NoWarn>NU5100</NoWarn>
	...
</PropertyGroup>

...
<!-- This is the target we defined above. It's purpose is to add all of our PackageReference and ProjectReference's runtime assets to our package output.  -->
<Target
	Name="CopyProjectReferencesToPackage"
	DependsOnTargets="ResolveReferences">
	<ItemGroup>
		<!-- The TargetPath is the path inside the package that the source file will be placed. This is already precomputed in the ReferenceCopyLocalPaths items' DestinationSubPath, so reuse it here. -->
		<BuildOutputInPackage
			Include="@(ReferenceCopyLocalPaths)"
			TargetPath="%(ReferenceCopyLocalPaths.DestinationSubPath)" />
	</ItemGroup>
</Target>

請勿將 Microsoft.Build.Utilities.Core 組件組合在一起

如上所述,此相依性將由 MSBuild 本身在執行階段提供,因此我們不需要將其組合至套件中。 若要這樣做,請將 ExcludeAssets="Runtime" 屬性新增至 PackageReference

...
<PackageReference 
	Include="Microsoft.Build.Utilities.Core"
	Version="17.0.0"
	PrivateAssets="all"
	ExcludeAssets="Runtime"
/>
...

產生並內嵌 deps.json 檔案

MSBuild 可以使用 deps.json 檔案來確保載入相依性的正確版本。 您必須新增一些 MSBuild 屬性才能產生檔案,因為其預設不會為程式庫產生該檔案。 然後,新增目標以將其包含在套件輸出中,類似於您針對套件相依性所做的作業。

<PropertyGroup>
	...
	<!-- Tell the SDK to generate a deps.json file -->
	<GenerateDependencyFile>true</GenerateDependencyFile>
	...
</PropertyGroup>

...
<!-- This target adds the generated deps.json file to our package output -->
<Target
		Name="AddBuildDependencyFileToBuiltProjectOutputGroupOutput"
		BeforeTargets="BuiltProjectOutputGroup"
		Condition=" '$(GenerateDependencyFile)' == 'true'">

	 <ItemGroup>
		<BuiltProjectOutputGroupOutput
			Include="$(ProjectDepsFilePath)"
			TargetPath="$(ProjectDepsFileName)"
			FinalOutputPath="$(ProjectDepsFilePath)" />
	</ItemGroup>
</Target>

在套件中包含 MSBuild 屬性和目標

如需本節的背景,請閱讀屬性和目標,以及如何在 NuGet 套件中包含屬性和目標

在某些情況下,您可能想要在取用您套件的專案中新增自訂組建目標或屬性,例如在建置期間執行自訂工具或程序。 您可以透過將 <package_id>.targets<package_id>.props 形式的檔案放置在專案的 build 資料夾中來完成此作業。

專案根 build 資料夾中的檔案會視為適用於所有目標架構。

在本節中,您將連接 .props.targets 檔案中的工作實作,這些檔案會包含在我們的 NuGet 套件中,並從參考專案自動載入。

  1. 在工作的專案檔中,AppSettingStronglyTyped.csproj,新增下列程式碼:

     <ItemGroup>
     	<!-- these lines pack the build props/targets files to the `build` folder in the generated package.
     		by convention, the .NET SDK will look for build\<Package Id>.props and build\<Package Id>.targets
     		for automatic inclusion in the build. -->
     	<Content Include="build\AppSettingStronglyTyped.props" PackagePath="build\" />
     	<Content Include="build\AppSettingStronglyTyped.targets" PackagePath="build\" />
     </ItemGroup>
    
  2. 建立 build 資料夾,並在該資料夾中新增兩個文字檔:AppSettingStronglyTyped.propsAppSettingStronglyTyped.targets。 在 Microsoft.Common.props 中,AppSettingStronglyTyped.props 會提早匯入,因此它無法使用較晚才定義的屬性。 因此,請避免參考尚未定義的屬性;它們會評估為空的。

    從 NuGet 套件匯入 .targets 檔案之後,會從 Microsoft.Common.targets 匯入 Directory.Build.targets。 因此,它可以覆寫大部分建置邏輯中定義的屬性和目標,或為所有專案設定屬性,而不論個別專案設定的內容為何。 請參閱匯入順序

    AppSettingStronglyTyped.props 包含工作,並使用預設值定義一些屬性:

     <?xml version="1.0" encoding="utf-8" ?>
     <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
     <!--defining properties interesting for my task-->
     <PropertyGroup>
     	<!--The folder where the custom task will be present. It points to inside the nuget package. -->
     	<_AppSettingsStronglyTyped_TaskFolder>$(MSBuildThisFileDirectory)..\tasks\netstandard2.0</_AppSettingsStronglyTyped_TaskFolder>
     	<!--Reference to the assembly which contains the MSBuild Task-->
     	<CustomTasksAssembly>$(_AppSettingsStronglyTyped_TaskFolder)\$(MSBuildThisFileName).dll</CustomTasksAssembly>
     </PropertyGroup>
    
     <!--Register our custom task-->
     <UsingTask TaskName="$(MSBuildThisFileName).AppSettingStronglyTyped" AssemblyFile="$(CustomTasksAssembly)"/>
    
     <!--Task parameters default values, this can be overridden-->
     <PropertyGroup>
     	<RootFolder Condition="'$(RootFolder)' == ''">$(MSBuildProjectDirectory)</RootFolder>
     	<SettingClass Condition="'$(SettingClass)' == ''">MySetting</SettingClass>
     	<SettingNamespace Condition="'$(SettingNamespace)' == ''">example</SettingNamespace>
     	<SettingExtensionFile Condition="'$(SettingExtensionFile)' == ''">mysettings</SettingExtensionFile>
     </PropertyGroup>
     </Project>
    
  3. 安裝套件時,會自動包含 AppSettingStronglyTyped.props 檔案。 接著,用戶端有可用的工作和一些預設值。 然而,它從未被使用過。 若要讓此程式碼運作,請在 AppSettingStronglyTyped.targets 檔案中定義一些目標,安裝套件時也會自動包含這些目標:

     <?xml version="1.0" encoding="utf-8" ?>
     <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    
     <!--Defining all the text files input parameters-->
     <ItemGroup>
     	<SettingFiles Include="$(RootFolder)\*.$(SettingExtensionFile)" />
     </ItemGroup>
    
     <!--A target that generates code, which is executed before the compilation-->
     <Target Name="BeforeCompile" Inputs="@(SettingFiles)" Outputs="$(RootFolder)\$(SettingClass).generated.cs">
     	<!--Calling our custom task-->
     	<AppSettingStronglyTyped SettingClassName="$(SettingClass)" SettingNamespaceName="$(SettingNamespace)" SettingFiles="@(SettingFiles)">
     		<Output TaskParameter="ClassNameFile" PropertyName="SettingClassFileName" />
     	</AppSettingStronglyTyped>
     	<!--Our generated file is included to be compiled-->
     	<ItemGroup>
     		<Compile Remove="$(SettingClassFileName)" />
     		<Compile Include="$(SettingClassFileName)" />
     	</ItemGroup>
     </Target>
    
     <!--The generated file is deleted after a general clean. It will force the regeneration on rebuild-->
     <Target Name="AfterClean">
     	<Delete Files="$(RootFolder)\$(SettingClass).generated.cs" />
     </Target>
     </Project>
    

    第一個步驟是建立 ItemGroup,代表要讀取的文字檔 (可能超過一個),而這將是我們的一些工作參數。 我們尋找的位置和延伸模組有預設值,但您可以覆寫在用戶端 MSBuild 專案檔中定義屬性的值。

    然後定義兩個 MSBuild 目標。 我們會延伸 MSBuild 流程,並覆寫預先定義的目標:

    • BeforeCompile:目標是呼叫自訂工作來產生類別,並包含要編譯的類別。 在核心編譯完成之前插入此目標中的工作。 輸入和輸出與累加建置相關的欄位。 如果所有輸出項目都是最新的,則 MSBuild 會跳過目標。 此目標的累加建置可以大幅改善組建的效能。 如果項目的輸出檔與輸入檔同齡或是前者較新,該項目則可視為最新狀態。

    • AfterClean:目標是在發生一般清除之後,刪除產生的類別檔案。 在叫用核心清除功能之後插入此目標中的工作。 當執行重建目標時,它強制執行重複程式碼產生步驟。

產生 NuGet 套件

若要產生 NuGet 套件,您可以使用 Visual Studio (以滑鼠右鍵按一下 [方案總管] 中的專案節點,然後選取 [套件])。 您也可以使用命令列來執行此動作。 瀏覽至工作專案檔 AppSettingStronglyTyped.csproj 所在的資料夾,然後執行下列命令:

// -o is to define the output; the following command chooses the current folder.
dotnet pack -o .

恭喜! 您已產生名為 \AppSettingStronglyTyped\AppSettingStronglyTyped\AppSettingStronglyTyped.1.0.0.nupkg 的 NuGet 套件。

該套件具有延伸模組 .nupkg,而且是壓縮的 zip 檔案。 您可以使用 zip 工具開啟。 .target.props 檔案位於 build 資料夾中。 .dll 檔案位於 lib\netstandard2.0\ 資料夾中。 AppSettingStronglyTyped.nuspec 檔案位於根層級。

(選擇性) 支援多目標

為了支援最廣泛的使用者群,您應該考慮同時支援 Full (.NET Framework) 和 Core (包括 .NET 5 和更新版本) MSBuild 散發套件。

對於「一般」.NET SDK 專案,多目標表示在專案檔中設定多個 TargetFrameworks。 當您執行此作業時,系統會觸發兩個 TargetFrameworkMonikers 組建,並且整體結果可以封裝為單一成品。

這不是 MSBuild 的完整故事。 MSBuild 有兩個主要運送車輛:Visual Studio 和 .NET SDK。 這些是非常不同的執行階段環境;一種在 .NET Framework 執行階段上執行,另一種在 CoreCLR 上執行。 這表示雖然您的程式碼是以 netstandard2.0 為目標,但您的工作邏輯可能會根據目前使用中的 MSBuild 執行階段類型而有所不同。 實際上,由於 .NET 5.0 和更新版本中有很多新的 API,因此對於多個 TargetFrameworkMonikers 的 MSBuild 工作原始碼的多目標,以及多個 MSBuild 執行階段類型的 MSBuild 目標邏輯的多目標都是有意義的。

多目標所需的變更

若要以多個 TargetFrameworkMonikers (TFM) 為目標:

  1. 將您的專案檔變更為使用 net472net6.0 TFM (後者可能會根據您想要鎖定的 SDK 層級而變更)。 您可能想要鎖定 netcoreapp3.1 目標,直到 .NET Core 3.1 不再支援為止。 當您執行此作業時,封裝資料夾結構會從 tasks/ 變更為 tasks/<TFM>/

    <TargetFrameworks>net472;net6.0</TargetFrameworks>
    
  2. 更新您的 .targets 檔案,以使用正確的 TFM 載入您的工作。 所需的 TFM 會根據您上述選擇的 .NET TFM 而進行變更,但對於目標為 net472net6.0 的專案,您會擁有如下的屬性:

<AppSettingStronglyTyped_TFM Condition=" '$(MSBuildRuntimeType)' != 'Core' ">net472</AppSettingStronglyTyped_TFM>
<AppSettingStronglyTyped_TFM Condition=" '$(MSBuildRuntimeType)' == 'Core' ">net6.0</AppSettingStronglyTyped_TFM>

此程式碼會使用 MSBuildRuntimeType 屬性作為作用中裝載環境的 Proxy。 設定此屬性之後,您可以在 UsingTask 中使用它來載入正確的 AssemblyFile

<UsingTask
    AssemblyFile="$(MSBuildThisFileDirectory)../tasks/$(AppSettingStronglyTyped_TFM)/AppSettingStronglyTyped.dll"
    TaskName="AppSettingStrongTyped.AppSettingStronglyTyped" />

下一步

許多工作都牽涉到呼叫可執行檔。 在某些情況下,您可以使用 Exec 工作,但如果 Exec 工作的限制是個問題,您也可以建立自訂工作。 下列教學課程會逐步解說這兩個選項,其中包含更真實的程式碼產生案例:建立自訂工作來產生 REST API 的用戶端程式碼。

或者,了解如何測試自訂工作。