次の方法で共有


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

このチュートリアルでは、コード生成を処理するカスタム タスクを 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 (.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 実装 (.NET Core など) の間でコードを共有する場合、ライブラリは .NET Standard 2.0 ターゲットにする必要があり、.NET Framework 上で実行される Visual Studio 内で実行する必要があります。 .NET Framework では、.NET Standard 2.1 はサポートされていません。

参照する MSBuild API のバージョンを選択する

カスタム タスクをコンパイルするときは、サポートする予定の Visual Studio や .NET SDK の最小バージョンと一致する MSBuild API (Microsoft.Build.*) のバージョンを参照する必要があります。 たとえば、Visual Studio 2019 でユーザーをサポートするには、MSBuild 16.11 に対してビルドする必要があります。

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 プロセスで MSBuild カスタム タスクを生成しないようにすることが重要です。 新しいプロジェクトは、完全に異なる Visual Studio ソリューションに配置する必要があります。または、新しいプロジェクトでは、事前に生成された dll を使用し、標準出力から再配置する必要があります。

  1. 新しい Visual Studio ソリューションで .NET コンソール プロジェクト MSBuildConsoleExample を作成します。

    タスクを配布する通常の方法は NuGet パッケージを使用することですが、開発とデバッグ中に、.props.targets に関するすべての情報をアプリケーションのプロジェクト ファイルに直接含め、タスクを他のユーザーに配布するときに NuGet 形式に移動できます。

  2. コード生成タスクを使用するようにプロジェクト ファイルを変更します。 このセクションのコード一覧には、タスクを参照し、タスクの入力パラメーターを設定した後、生成されたコード ファイルが想定どおりに削除されるように、クリーンおよびリビルド操作を処理するためのターゲットを記述した後の変更されたプロジェクト ファイルが表示されます。

    タスクは、UsingTask 要素 (MSBuild)を使用して登録されます。 UsingTask 要素はタスクを登録します。タスクの名前と、タスク クラスを含むアセンブリを検索して実行する方法を MSBuild に指示します。 アセンブリ パスは、プロジェクト ファイルに対する相対パスです。

    PropertyGroup には、タスクで定義されたプロパティに対応するプロパティ定義が含まれています。 これらのプロパティは属性を使用して設定され、タスク名は要素名として使用されます。

    TaskName は、アセンブリから参照するタスクの名前です。 この属性では、常に完全に指定された名前空間を使用する必要があります。 AssemblyFile は、アセンブリのファイル パスです。

    タスクを呼び出すには、タスクを適切なターゲットに追加します(この場合は GenerateSetting

    ターゲット ForceGenerateOnRebuild は、生成されたファイルを削除することで、クリーン操作とリビルド操作を処理します。 CoreClean 属性を AfterTargetsに設定することで、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 スタイルのプロジェクトでは、プロジェクト ファイルの最後の行の後にターゲットが暗黙的にインポートされます。つまり、インポートを手動で指定しない限り、既定のターゲットをオーバーライドすることはできません。 「定義済みのターゲットをオーバーライドする」を参照してください。

    Inputs 属性と Outputs 属性は、インクリメンタル ビルドの情報を提供することで、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 がそのアセンブリを含めてしまうため、lib/<target framework> フォルダーにはパッケージ化されないのです。
  • 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>
    <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
    ...
</PropertyGroup>

依存関係が出力ディレクトリにコピーされるようにするには、CopyLocalLockFileAssemblies プロパティが必要です。

依存関係をプライベートとしてマークする

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

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

ここで、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>

依存関係をパッケージにまとめて組み込む

また、依存関係のランタイム資産をタスク パッケージに埋め込む必要もあります。 これには、依存関係を BuildOutputInPackage ItemGroup に追加する MSBuild ターゲットと、それらの BuildOutputInPackage 項目のレイアウトを制御するいくつかのプロパティの 2 つの部分があります。 このプロセス の詳細については、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>
    <!-- Suppress NuGet warning NU5128. -->
    <SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
    ...
</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 パッケージ にプロパティとターゲットを含める方法について説明します。

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

ビルド フォルダー プロジェクト ルート内のファイルは、すべてのターゲット フレームワークに適したと見なされます。

このセクションでは、nuGet パッケージに含まれ、参照元のプロジェクトから自動的に読み込まれる、.props ファイルと .targets ファイルのタスク実装を結び付けます。

  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. ビルド フォルダーを作成し、そのフォルダーに 2 つのテキスト ファイル (AppSettingStronglyTyped.propsAppSettingStronglyTyped.targets) を追加します。 AppSettingStronglyTyped.props は Microsoft.Common.props 早い段階でインポートされ、後で定義されたプロパティは使用できません。 そのため、まだ定義されていないプロパティを参照することは避けてください。それらは空と評価されます。

    Directory.Build.targets は、NuGet パッケージから ファイルをインポートした後、.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: 目的は、カスタム タスクを呼び出してクラスを生成し、コンパイルするクラスを含めます。 このターゲットのタスクは、コア コンパイルが行われる前に挿入されます。 入力フィールドと出力フィールドは、インクリメンタル ビルド 関連しています。 すべての出力項目が -date up-to場合、MSBuild はターゲットをスキップします。 ターゲットのこの増分ビルドにより、ビルドのパフォーマンスが大幅に向上します。 項目は、出力ファイルの有効期間が入力ファイルまたはファイルと同じか新しい場合、-date up-toと見なされます。

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

NuGet パッケージを生成する

NuGet パッケージを生成するには、Visual Studio を使用できます (ソリューション エクスプローラーの でプロジェクト ノード右クリックし、Packを選択します)。 コマンド ラインを使用して実行することもできます。 タスク プロジェクト ファイル 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 の両方に対してビルドがトリガーされ、全体的な結果を 1 つの成果物としてパッケージ化できます。

これは MSBuild の完全なストーリーではありません。 MSBuild には、Visual Studio と .NET SDK という 2 つの主要な出荷車両があります。 これらは非常に異なるランタイム環境です。1 つは .NET Framework ランタイムで実行され、もう 1 つは CoreCLR 上で実行されます。 つまり、コードは netstandard2.0 をターゲットにすることができますが、現在使用されている MSBuild ランタイムの種類に基づいてタスク ロジックに違いがある可能性があります。 実際には、.NET 5.0 以降には非常に多くの新しい API があるため、複数の TargetFrameworkMonikers の MSBuild タスク ソース コードのマルチターゲットと、複数の MSBuild ランタイム型の MSBuild ターゲット ロジックのマルチターゲットの両方が理にかなっています。

マルチターゲットに必要な変更

複数の TargetFrameworkMonikers (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 用のクライアント コードを生成するカスタム タスクの作成という両方のオプションについて説明します。

ビルド でコード生成を使用する

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

カスタム MSBuild タスク をテストする