チュートリアル: コード生成用のカスタム タスクを作成する

このチュートリアルでは、コード生成を処理する MSBuild のカスタム タスクを C# で作成し、ビルドでそのタスクを使います。 この例では、MSBuild を使ってクリーンとリビルド操作を処理する方法を示します。 また、この例では、入力ファイルが変更されたときだけコードを生成するように、インクリメンタル ビルドをサポートする方法も示しています。 この示されている手法は、さまざまなコード生成シナリオに適用できます。 また、この手順では、NuGet を使ってタスクを配布用にパッケージ化しています。さらに、このチュートリアルには、トラブルシューティングのエクスペリエンスを改善させるために BinLog ビューアーを使うオプションの手順が含まれています。

前提条件

タスク、ターゲット、プロパティなどの MSBuild の概念について理解している必要があります。 「MSBuild の概念」を参照してください。

この例では、MSBuild が必要です。これは Visual Studio と一緒にインストールされますが、個別にインストールすることもできます。 Visual Studio なしで MSBuild をダウンロードするに関するページを参照してください。

コード例の概要

この例では、設定する値を含む入力テキスト ファイルを受け取り、これらの値を作成するコードを含む C# コード ファイルを作成します。 これは単純な例ですが、同様の基本的手法を、より複雑なコード生成シナリオに適用することができます。

このチュートリアルでは、AppSettingStronglyTyped という名前の MSBuild カスタム タスクを作成します。 このタスクは一連のテキスト ファイルを読み込み、各ファイルには次のような形式の行が含まれます。

propertyName:type:defaultValue

このコードはすべての定数を含む C# クラスを生成します。 問題が発生した場合はビルドを停止し、問題を診断するのに十分な情報をユーザーに提供する必要があります。

このチュートリアルの完全なサンプル コードは、GitHub の .NET サンプル リポジトリの「Custom task - code generation (カスタム タスク - コード生成)」にあります。

AppSettingStronglyTyped プロジェクトを作成する

.NET Standard クラス ライブラリを作成します。 フレームワークは .NET Standard 2.0 である必要があります。

Visual Studio で使われるフル MSBuild と、.NET Core コマンド ラインにバンドルされているポータブル MSBuild の違いに注意してください。

  • フル 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 Core などの他の .NET 実装間でコードを共有する場合、ライブラリは .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. 3 つのプロパティを追加します。 これらのプロパティは、ユーザーがクライアント プロジェクトでタスクを使うときに設定するタスクのパラメーターを定義します。

     //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 を、そうでない場合は false を返します。 TaskMicrosoft.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. .NET コンソール プロジェクト MSBuildConsoleExample を新しい Visual Studio Solution に作成します。

    タスクを配布する通常の方法は 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>
    

    Note

    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 Binary and Structured Log Viewer で開くことができます。 オプション /t:rebuild はリビルド ターゲットの実行を意味します。 これにより、生成されたコード ファイルが強制的に再生成されます。

お疲れさまでした。 コードを生成するタスクをビルドして、それをビルドで使いました。

配布するタスクのパッケージ化

カスタム タスクを少数のプロジェクトまたは 1 つのソリューションで使う必要がある場合は、未加工のアセンブリとしてタスクを使うだけで済みますが、タスクを他の場所で使ったり他のユーザーと共有するために準備する場合は、NuGet パッケージとして使うことをお勧めします。

MSBuild タスク パッケージには、ライブラリの NuGet パッケージと主な違いがいくつかあります。

  • 使用するプロジェクトに対して依存関係を公開するのではなく、独自のアセンブリ依存関係をバンドルする必要があります
  • 必要なアセンブリは lib/<target framework> フォルダーにパッケージ化されません。そうすると、NuGet によって、そのタスクを使用するすべてのパッケージにアセンブリが追加されるためです
  • Microsoft.Build アセンブリに対して コンパイル する必要だけがあります。実行時に実際の MSBuild エンジンによって提供されるため、パッケージに含める必要はありません
  • MSBuild がタスクの依存関係 (特にネイティブ依存関係) を一貫した方法で読み込むのに役立つ、特別な .deps.json ファイルが生成されます

これらの目標をすべて達成するために、使い慣れた標準プロジェクト ファイルにいくつかの変更を加える必要があります。

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 プロパティの完全な一覧については、Pack のドキュメントを参照してください。

<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 タスクの依存関係は、パッケージ内にパッケージ化する必要があります。通常のパッケージ参照として表現することはできません。 パッケージは、通常の依存関係を外部ユーザーに公開しません。 これには 2 つの手順が必要です。アセンブリをプライベートとしてマークすることと、それらを生成されたパッケージに実際に埋め込むことです。 この例では、タスクが動作するために Microsoft.Extensions.DependencyInjection に依存することを前提としているため、PackageReferenceMicrosoft.Extensions.DependencyInjection にバージョン 6.0.0 で追加します。

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

次に、この Task プロジェクトのすべての依存関係 (PackageReferenceProjectReference の両方) を、PrivateAssets="all" 属性でマークします。 これにより、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>

依存関係をパッケージにバンドルする

依存関係のランタイム アセットを Task パッケージに埋め込む必要もあります。 これには 2 つの部分があります。依存関係を 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 ファイルを生成して埋め込む

deps.json ファイルは、依存関係の正しいバージョンが読み込まれるようにするために、MSBuild で使用できます。 いくつかの 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 パッケージにプロパティとターゲットを含める方法に関するページを参照してください。

ビルド時のカスタム ツールまたはプロセスの実行など、場合によっては、パッケージを使用するプロジェクト内のカスタム ビルド ターゲットまたはプロパティを追加する必要があります。 これは、プロジェクトの build フォルダー内に <package_id>.targets または <package_id>.props の形式でファイルを配置することによって行います。

プロジェクト ルートの 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 フォルダーを作成し、そのフォルダーに 2 つのテキスト ファイル AppSettingStronglyTyped.propsAppSettingStronglyTyped.targets を追加します。 AppSettingStronglyTyped.propsMicrosoft.Common.props で早期にインポートされ、後で定義されるプロパティを使用することはできません。 そのため、まだ定義されていないプロパティを参照しないようにしてください。評価が空になります。

    Directory.Build.targets は、NuGet パッケージから .targets ファイルがインポートされた後に Microsoft.Common.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 プロジェクト ファイルでプロパティを定義する値をオーバーライドすることができます。

    次に、2 つの MSBuild ターゲットを定義します。 MSBuild プロセスを拡張し、事前定義されたターゲットをオーバーライドします。

    • BeforeCompile: 目的は、カスタム タスクを呼び出してクラスを生成し、コンパイルするクラスを含めることです。 このターゲットのタスクは、コア コンパイルが完了する前に挿入されます。 Input と Output フィールドは、インクリメンタルビルドに関連しています。 すべての出力項目が最新の状態である場合、MSBuild はターゲットをスキップします。 このターゲットのインクリメンタル ビルドにより、ビルドのパフォーマンスを大幅に向上させることができます。 項目が最新の状態であると見なされるのは、その出力ファイルが入力ファイルと同じタイム スタンプであるかそれよりも新しい場合です。

    • AfterClean: 目的は、一般的なクリーンが行われた後に、生成されたクラス ファイルを削除することです。 このターゲットのタスクは、コア クリーン機能が呼び出された後に挿入されます。 Rebuild ターゲットが実行されたときに、コード生成手順が強制的に繰り返されます。

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 プロジェクトの場合、複数バージョン対応とは、プロジェクト ファイルで複数の TargetFramework を設定することを意味します。 これを行うと、両方の TargetFrameworkMoniker に対してビルドがトリガーされ、全体的な結果を 1 つの成果物としてパッケージ化できます。

MSBuild の機能はこれだけではありません。 MSBuild の主要な出荷方法には、Visual Studio と .NET SDK の 2 つがあります。 これらは非常に異なるランタイム環境です。1 つは .NET Framework ランタイムで実行され、もう 1 つは CoreCLR で実行されます。 つまり、コードでは netstandard2.0 を対象にできますが、タスクのロジックは現在使われている MSBuild ランタイムの種類により異なる可能性があります。 実際には、.NET 5.0 以降には非常に多くの新しい API があるため、MSBuild タスクのソース コードを複数の TargetFrameworkMoniker に対応させることと、MSBuild のターゲット ロジックを複数の MSBuild ランタイムの種類に対応させることの両方を行うのが理にかなっています。

複数バージョン対応に必要な変更

複数の TargetFrameworkMoniker (TFM) に対応するには:

  1. net472net6.0 の TFM (後者は、ターゲットにする SDK レベルにより変わる可能性があります) を使うように、プロジェクト ファイルを変更します。 .NET Core 3.1 がサポート対象外になるまでは、netcoreapp3.1 をターゲットにできます。 これを行うと、パッケージ フォルダーの構造が tasks/ から tasks/<TFM>/ に変わります。

    <TargetFrameworks>net472;net6.0</TargetFrameworks>
    
  2. 正しい TFM を使ってタスクを読み込むように、.targets ファイルを更新します。 必要な TFM は上で選んだ .NET TFM によって変わりますが、net472net6.0 に対応するプロジェクトの場合は、次のようなプロパティがあります。

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

このコードでは、アクティブなホスティング環境に対するプロキシとして MSBuildRuntimeType プロパティを使います。 このプロパティを設定したら、それを UsingTask で使って正しい AssemblyFile を読み込むことができます。

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

次のステップ

実行可能ファイルの呼び出しには多くのタスクが関連します。 シナリオによっては Exec タスクを使用できますが、Exec タスクの制限が問題になる場合は、カスタム タスクを作成することもできます。 次のチュートリアルでは、より現実的なコード生成シナリオである、REST API 用のクライアント コードを生成するカスタム タスクの作成を通して、両方の選択肢について説明します。

あるいは、カスタム タスクのテスト方法について説明します。