Tutorial: Erstellen eines benutzerdefinierten Tasks für Codegenerierung

In diesem Tutorial erstellen Sie einen benutzerdefinierten Task in MSBuild in C#, der Codegenerierung verarbeitet, und verwenden den Task dann in einem Build. In diesem Beispiel wird veranschaulicht, wie Sie MSBuild zum Verarbeiten der Bereinigungs- und Neuerstellungsvorgänge verwenden können. Das Beispiel zeigt auch, wie inkrementelle Builds unterstützt werden, sodass der Code nur generiert wird, wenn sich die Eingabedateien geändert haben. Die gezeigten Techniken gelten für eine Vielzahl von Codegenerierungsszenarien. Die Schritte zeigen auch die Verwendung von NuGet zum Packen des Tasks für die Verteilung, und das Tutorial enthält einen optionalen Schritt zur Verwendung des BinLog-Viewers, um die Problembehandlung zu verbessern.

Voraussetzungen

Sie sollten mit den MSBuild-Konzepten wie Tasks, Zielen und Eigenschaften vertraut sein. Weitere Informationen finden Sie unter MSBuild-Konzepte.

Die Beispiele erfordern MSBuild. Diese Engine wird mit Visual Studio installiert, kann aber auch separat installiert werden. Weitere Informationen finden Sie unter Herunterladen von MSBuild ohne Visual Studio.

Einführung in das Codebeispiel

Das Beispiel verwendet eine Eingabetextdatei, die festzulegende Werte enthält, und erstellt eine C#-Codedatei mit Code, der diese Werte generiert. Das ist zwar ein einfaches Beispiel, aber dieselben grundlegenden Techniken lassen sich auch auf komplexere Szenarien der Codegenerierung anwenden.

In diesem Tutorial erstellen Sie einen benutzerdefinierten MSBuild-Task namens AppSettingStronglyTyped. Der Task liest einen Satz von Textdateien und jede Datei mit Zeilen im folgenden Format:

propertyName:type:defaultValue

Der Code generiert eine C#-Klasse mit allen Konstanten. Ein Problem sollte den Buildvorgang unterbrechen und dem Benutzer genügend Informationen geben, um das Problem zu diagnostizieren.

Den vollständigen Beispielcode für dieses Tutorial finden Sie unter Custom task – code generation (Benutzerdefinierter Task – Codegenerierung) im Repository mit .NET-Beispielen auf GitHub.

Erstellen des AppSettingStronglyTyped-Projekts

Erstellen Sie eine .NET Standard-Klassenbibliothek. Das Framework sollte .NET Standard 2.0 sein.

Beachten Sie den Unterschied zwischen dem vollständigen MSBuild (der von Visual Studio verwendeten Version) und portablem MSBuild, das in der .NET Core-Befehlszeile gebündelt ist.

  • Vollständiges MSBuild: Diese Version von MSBuild ist in der Regel in Visual Studio enthalten. Wird in .NET Framework ausgeführt. Visual Studio verwendet diese Version, wenn Sie Erstellen für Ihre Projektmappe oder Ihr Projekt ausführen. Diese Version ist auch in einer Befehlszeilenumgebung verfügbar, z. B. in der Visual Studio Developer-Eingabeaufforderung oder in PowerShell.
  • .NET MSBuild: Diese Version von MSBuild ist in der .NET Core-Befehlszeile gebündelt. Sie wird unter .NET Core ausgeführt. Visual Studio ruft diese Version von MSBuild nicht direkt auf. Es werden nur Projekte unterstützt, die mithilfe von Microsoft.NET.Sdk erstellt werden.

Wenn Sie Code zwischen .NET Framework und einer anderen .NET-Implementierung (z. B. .NET Core) teilen möchten, sollte Ihre Bibliothek auf .NET Standard 2.0 abzielen, und die Ausführung sollte in Visual Studio erfolgen, das in .NET Framework ausgeführt wird. .NET Standard 2.1 wird von .NET Framework nicht unterstützt.

Erstellen des benutzerdefinierten MSBuild-Tasks AppSettingStronglyTyped

Der erste Schritt besteht darin, den benutzerdefinierten MSBuild-Task zu erstellen. Die Informationen zum Schreiben eines benutzerdefinierten MSBuild.Tasks können Ihnen helfen, die folgenden Schritte zu verstehen. Eine benutzerdefinierter MSBuild-Task ist eine Klasse, die die ITask-Schnittstelle implementiert.

  1. Fügen Sie einen Verweis auf das NuGet-Paket Microsoft.Build.Utilities.Core hinzu, und erstellen Sie dann eine Klasse namens AppSettingStronglyTyped, die von Microsoft.Build.Utilities.Task abgeleitet ist.

  2. Fügen Sie drei Eigenschaften hinzu. Diese Eigenschaften definieren die Parameter des Tasks, die Benutzer festlegen, wenn sie den Task in einem Clientprojekt verwenden:

     //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; }
    

    Der Task verarbeitet die settingFiles und generiert eine SettingNamespaceName.SettingClassName-Klasse. Die generierte Klasse verfügt über einen Satz von Konstanten, die auf dem Inhalt der Textdatei basieren.

    Die Taskausgabe sollte eine Zeichenfolge sein, die den Dateinamen des generierten Codes angibt:

     // The filename where the class was generated
     [Output]
     public string ClassNameFile { get; set; }
    
  3. Wenn Sie einen benutzerdefinierten Task erstellen, erben Sie von Microsoft.Build.Utilities.Task. Um den Task zu implementieren, überschreiben Sie die Execute()-Methode. Die Execute-Methode gibt true zurück, wenn der Task erfolgreich ist, andernfalls false. Task implementiert Microsoft.Build.Framework.ITaskund stellt Standardimplementierungen einiger ITask-Member und darüber hinaus einige Protokollierungsfunktionen zur Verfügung. Es ist wichtig, den Status in das Protokoll auszugeben, um den Task zu diagnostizieren und Fehler zu beheben, insbesondere wenn ein Problem auftritt und der Task ein Fehlerergebnis (false) zurückgeben muss. Bei einem Fehler signalisiert die Klasse den Fehler, indem TaskLoggingHelper.LogError aufgerufen wird.

     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;
     }
    

    Die Task-API ermöglicht die Rückgabe von FALSE und gibt so einen Fehler an, ohne dem Benutzer anzuzeigen, was nicht funktioniert hat. Am besten ist es, !Log.HasLoggedErrors anstelle eines booleschen Codes zurückzugeben und einen Fehler zu protokollieren, wenn etwas nicht funktioniert.

Protokollfehler

Die bewährte Methode beim Protokollieren von Fehlern besteht in der Bereitstellung von Details wie der Zeilennummer und eines eindeutigen Fehlercodes beim Protokollieren eines Fehlers. Der folgende Code analysiert die Texteingabedatei und verwendet die TaskLoggingHelper.LogError-Methode mit der Zeilennummer in der Textdatei, die den Fehler erzeugt hat.

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);
}

Mithilfe der im vorherigen Code gezeigten Techniken werden Fehler in der Syntax der Texteingabedatei als Buildfehler mit hilfreichen Diagnoseinformationen angezeigt:

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)

Wenn Sie Ausnahmen in Ihrem Taskabfangen, verwenden Sie die TaskLoggingHelper.LogErrorFromException-Methode. Dadurch wird die Fehlerausgabe verbessert, z. B. durch Abrufen der Aufrufliste, in der die Ausnahme ausgelöst wurde.

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;
}

Die Implementierung der anderen Methoden, die diese Eingaben verwenden, um den Text für die generierte Codedatei zu erstellen, wird hier nicht gezeigt. Weitere Informationen finden Sie in AppSettingStronglyTyped.cs im Beispielrepository.

Der Beispielcode generiert C#-Code während des Buildprozesses. Der Task verhält sich wie jede andere C#-Klasse. Wenn Sie dieses Tutorial abgeschlossen haben, können Sie ihn also anpassen und jede Funktionalität hinzufügen, die für Ihr eigenes Szenario erforderlich ist.

Generieren einer Konsolen-App und Verwenden des benutzerdefinierten Tasks

In diesem Abschnitt erstellen Sie eine standardmäßige .NET Core-Konsolen-App, die den Task verwendet.

Wichtig

Es ist wichtig, das Generieren eines benutzerdefinierten MSBuild-Tasks in demselben MSBuild-Prozess, der ihn nutzen wird, zu vermeiden. Das neue Projekt sollte sich in einer vollständig anderen Visual Studio-Projektmappe befinden, oder das neue Projekt verwendet eine vorab generierte DLL und wurde von der Standardausgabe getrennt.

  1. Erstellen Sie das .NET-Konsolenprojekt MSBuildConsoleExample in einer neuen Visual Studio-Projektmappe.

    Die normale Methode zum Verteilen eines Tasks besteht in einem NuGet-Paket. Während der Entwicklung und dem Debuggen können Sie jedoch alle Informationen zu .props und .targets direkt in die Projektdatei Ihrer Anwendung einschließen übernehmen und dann zum NuGet-Format wechseln, wenn Sie den Task an andere Benutzer verteilen.

  2. Ändern Sie die Projektdatei so, dass der Codegenerierungstask verwendet wird. Die Codeauflistung in diesem Abschnitt zeigt die geänderte Projektdatei nach dem Verweisen auf den Task, dem Festlegen der Eingabeparameter für den Task und dem Schreiben der Ziele für die Verarbeitung von Bereinigungs- und Neuerstellungsvorgängen, damit die generierte Codedatei wie erwartet entfernt wird.

    Tasks werden über das UsingTask-Element (MSBuild) registriert. Das UsingTask-Element registriert den Task. Es teilt MSBuild den Namen des Tasks mit und gibt an, wie die Assembly, die die Taskklasse enthält, zu finden und auszuführen ist. Der Assemblypfad ist relativ zur Projektdatei.

    PropertyGroup enthält die Eigenschaftendefinitionen, die den im Task definierten Eigenschaften entsprechen. Diese Eigenschaften werden mithilfe von Attributen festgelegt, und der Taskname wird als Elementname verwendet.

    TaskName ist der Name des Tasks, auf den aus der Assembly verwiesen wird. Dieses Attribut sollte immer vollständig angegebene Namespaces verwenden. AssemblyFile ist der Dateipfad der Assembly.

    Fügen Sie zum Aufrufen des Tasks den Task dem entsprechenden Ziel hinzu, in diesem Fall GenerateSetting.

    Das Ziel ForceGenerateOnRebuild verarbeitet die Bereinigungs- und Neuerstellungsvorgänge, indem die generierte Datei gelöscht wird. Die Ausführung wird nach dem CoreClean-Ziel festgelegt, indem das AfterTargets-Attribut auf CoreClean festgelegt wird.

     <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>
    

    Hinweis

    Anstatt ein Ziel wie CoreClean zu überschreiben, verwendet dieser Code eine andere Methode, um die Ziele (BeforeTarget und AfterTarget) zu sortieren. Projekte im SDK-Stil verfügen über einen impliziten Import von Zielen nach der letzten Zeile der Projektdatei. Dies bedeutet, dass Sie Standardziele nur dann überschreiben können, wenn Sie Ihre Importe manuell angeben. Weitere Informationen finden Sie unter Überschreiben vordefinierter Ziele.

    Die Inputs- und Outputs-Attribute helfen MSBuild effizienter zu sein, indem sie Informationen für inkrementelle Builds bereitstellen. Die Datumsangaben der Eingaben werden mit den Ausgaben verglichen, um zu ermitteln, ob das Ziel ausgeführt werden muss oder ob die Ausgabe des vorherigen Buildvorgangs wiederverwendet werden kann.

  3. Erstellen Sie die Eingabetextdatei mit der Erweiterung, die für die Erkennung definiert ist. Erstellen Sie mithilfe der Standarderweiterung MyValues.mysettings im Stammverzeichnis mit folgendem Inhalt:

     Greeting:string:Hello World!
    
  4. Führen Sie den Buildvorgag erneut aus. Die generierte Datei sollte erstellt und kompiliert werden. Überprüfen Sie den Projektordner auf die Datei MySetting.generated.cs.

  5. Da sich die Klasse MySetting im falschen Namespace befindet, nehmen Sie jetzt eine Änderung vor, um unseren App-Namespace zu verwenden. Öffnen Sie die Projektdatei, und fügen Sie den folgenden Code hinzu:

     <PropertyGroup>
     	<SettingNamespace>MSBuildConsoleExample</SettingNamespace>
     </PropertyGroup>
    
  6. Kompilieren Sie den Code erneut, und beachten Sie, dass sich die -Klasse im MSBuildConsoleExample-Namespace befindet. Auf diese Weise können Sie den generierten Klassennamen (SettingClass), die Texterweiterungsdateien (SettingExtensionFile), die als Eingabe verwendet werden sollen, und den Speicherort (RootFolder) neu definieren, wenn Sie möchten.

  7. Öffnen Sie Program.cs, und ändern Sie den hartcodierten Text „Hello World!!“ in die benutzerdefinierte Konstante:

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

Führen Sie das Programm aus. Es gibt die Begrüßung aus der generierten Klasse aus.

(Optional) Protokollieren von Ereignissen während des Buildprozesses

Es ist möglich, den Code mithilfe eines Befehlszeilenbefehls zu kompilieren. Navigieren Sie zum Projektordner. Sie verwenden die Option -bl (binäres Protokoll), um ein binäres Protokoll zu generieren. Das binäre Protokoll enthält nützliche Informationen, um zu erfahren, was während des Buildprozesses vor sich geht.

# 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

Beide Befehle generieren eine Protokolldatei msbuild.binlog, die mit der binären und strukturierten MSBuild-Protokollanzeige geöffnet werden kann. Die Option /t:rebuild bedeutet, dass das Neuerstellungsziel ausgeführt werden soll. Sie erzwingt die Neugenerierung der generierten Codedatei.

Herzlichen Glückwunsch! Sie haben einen Task erstellt, der Code generiert, und ihn in einem Buildvorgang verwendet.

Packen des Tasks für die Verteilung

Wenn Sie Ihren benutzerdefinierten Task nur in wenigen Projekten oder in einer einzelnen Projektmappe verwenden müssen, ist die Nutzung des Tasks als Rohassembly möglicherweise alles, was Sie benötigen. Die beste Möglichkeit, ihren Task für die Verwendung an anderer Stelle vorzubereiten oder mit anderen Benutzern zu teilen, ist jedoch ein NuGet-Paket.

MSBuild-Taskpakete weisen einige wichtige Unterschiede im Vergleich zu NuGet-Bibliothekspaketen auf:

  • Sie müssen ihre eigenen Assemblyabhängigkeiten bündeln, anstatt diese Abhängigkeiten für das nutzende Projekt offenzulegen
  • Sie packen keine erforderlichen Assemblys in einem Ordner lib/<target framework>, denn das würde NuGet dazu veranlassen, die Assemblys in jedes Paket einzubinden, das den Task nutzt.
  • Sie müssen nur mit den Microsoft.Build-Assemblys kompiliert werden – zur Laufzeit werden diese vom tatsächlichen MSBuild-Modul bereitgestellt und müssen daher nicht in das Paket eingebunden werden.
  • Sie generieren eine spezielle .deps.json-Datei, die MSBuild hilft, die Abhängigkeiten des Tasks (insbesondere systemeigene Abhängigkeiten) in konsistenter Weise zu laden.

Um all diese Ziele zu erreichen, müssen Sie einige Änderungen an der Standardprojektdatei vornehmen, die über das hinausgehen, womit Sie vielleicht schon vertraut sind.

Erstellen eines NuGet-Pakets

Das Erstellen eines NuGet-Pakets ist der empfohlene Weg, um Ihren benutzerdefinierten Task an andere Benutzer zu verteilen.

Vorbereiten der Erstellung des Pakets

Um die Erstellung eines NuGet-Pakets vorzubereiten, nehmen Sie einige Änderungen an der Projektdatei vor, um die Details anzugeben, die das Paket beschreiben. Die ursprüngliche Projektdatei, die Sie erstellt haben, ähnelt dem folgenden Code:

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

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

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

</Project>

Um ein NuGet-Paket zu generieren, fügen Sie den folgenden Code hinzu, um die Eigenschaften für das Paket festzulegen. In der Paketdokumentation finden Sie eine vollständige Liste der unterstützten MSBuild Eigenschaften:

<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>

Markieren von Abhängigkeiten als privat

Die Abhängigkeiten Ihres MSBuild-Tasks müssen innerhalb des Pakets gepackt werden. Sie können nicht als normale Paketverweise ausgedrückt werden. Das Paket macht keine regulären Abhängigkeiten für externe Benutzer verfügbar. Dazu sind zwei Schritte erforderlich: die Kennzeichnung Ihrer Assemblys als privat und die tatsächliche Einbettung in das generierte Paket. Für dieses Beispiel nehmen wir an, dass Ihr Task von Microsoft.Extensions.DependencyInjection abhängt, um zu funktionieren, also fügen Sie Microsoft.Extensions.DependencyInjection bei Version 6.0.0 ein PackageReference-Element zu hinzu.

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

Markieren Sie nun jede Abhängigkeit dieses Taskprojekts (PackageReference und ProjectReference) mit dem PrivateAssets="all"-Attribut. Dadurch wird NuGet informiert, dass diese Abhängigkeiten für nutzende Projekte überhaupt nicht offengelegt werden. Weitere Informationen zum Steuern von Abhängigkeitsressourcen finden Sie in der NuGet-Dokumentation.

<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>

Bündeln von Abhängigkeiten im Paket

Sie müssen auch die Laufzeitressourcen unserer Abhängigkeiten in das Taskpaket einbetten. Dies ist eine zweiteilige Aufgabe: ein MSBuild-Ziel, das unsere Abhängigkeiten zur BuildOutputInPackage-ItemGroup hinzufügt, und einige Eigenschaften, die das Layout dieser BuildOutputInPackage-Elemente steuern. Weitere Informationen zu diesem Vorgang finden Sie in der NuGet-Dokumentation.

<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>

Bündeln Sie die Microsoft.Build.Utilities.Core-Assembly nicht.

Wie bereits oben erwähnt, wird diese Abhängigkeit von MSBuild selbst zur Laufzeit bereitgestellt, sodass wir sie nicht in das Paket einbinden müssen. Fügen Sie dazu PackageReference das ExcludeAssets="Runtime"-Attribut hinzu.

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

Generieren und Einbetten einer deps.json-Datei

Die deps.json-Datei kann von MSBuild verwendet werden, um sicherzustellen, dass die richtigen Versionen Ihrer Abhängigkeiten geladen werden. Sie müssen einige MSBuild-Eigenschaften hinzufügen, damit die Datei generiert wird, da sie nicht standardmäßig für Bibliotheken generiert wird. Fügen Sie dann ein Ziel hinzu, um es in unsere Paketausgabe einzuschließen, ähnlich wie für die Paketabhängigkeiten.

<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>

Einbeziehen von MSBuild-Eigenschaften und -Zielen in einem Paket

Hintergrundinformationen zu diesem Abschnitt finden Sie unter Eigenschaften und Ziele und sowie unter Einbeziehen von Eigenschaften und Zielen in ein NuGet-Paket.

In einigen Fällen (z.B. beim Ausführen eines benutzerdefinierten Tools oder Prozesses während des Buildvorgangs) sollten Sie benutzerdefinierte Buildziele oder -eigenschaften zu Projekten hinzufügen, die Ihr Paket nutzen. Platzieren Sie dazu Dateien im Format <package_id>.targets oder <package_id>.props im Ordner build des Projekts.

Dateien im Ordner build des Stammverzeichnisses gelten als für alle Zielframeworks geeignet.

In diesem Abschnitt erstellen Sie die Taskimplementierung in .props- und .targets-Dateien, die in unserem NuGet-Paket enthalten sein werden und automatisch von einem referenzierenden Projekt geladen werden.

  1. Fügen Sie in der Projektdatei des Tasks (AppSettingStronglyTyped.csproj) den folgenden Code hinzu:

     <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. Erstellen Sie einen Ordner build, und fügen Sie in diesem Ordner zwei Textdateien hinzu: AppSettingStronglyTyped.props und AppSettingStronglyTyped.targets. AppSettingStronglyTyped.props wird zu einem frühen Zeitpunkt in Microsoft.Common.props importiert und später definierte Eigenschaften sind für diese Datei nicht verfügbar. Vermeiden Sie deshalb das Verweisen auf Eigenschaften, die noch nicht definiert sind. Sie würden als leer ausgewertet.

    Directory.Build.targets wird aus Microsoft.Common.targets importiert, nachdem .targets-Dateien aus NuGet-Paketen importiert wurden. Die Datei kann also Eigenschaften und Ziele, die in den meisten Buildlogiken definiert sind, außer Kraft setzen oder Eigenschaften für alle Ihre Projekte festlegen, und zwar unabhängig davon, was die einzelnen Projekte festlegen. Weitere Informationen finden Sie unter Importreihenfolge.

    AppSettingStronglyTyped.props enthält den Task und definiert einige Eigenschaften mit Standardwerten:

     <?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. Die Datei AppSettingStronglyTyped.props wird automatisch eingebunden, wenn das Paket installiert wird. Anschließend stehen dem Client der Task und einige Standardwerte zur Verfügung. Sie werden jedoch nie verwendet. Um diesen Code in Aktion zu sehen, definieren Sie einige Ziele in der Datei AppSettingStronglyTyped.targets, die bei der Installation des Pakets ebenfalls automatisch eingebunden wird:

     <?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>
    

    Der erste Schritt ist die Erstellung einer ItemGroup, die die zu lesenden Textdateien darstellt (es können mehrere Dateien sein), und sie wird ein Teil des Taskparameters sein. Es gibt Standardwerte für den Speicherort und die Erweiterung, nach denen wir suchen, aber Sie können die Werte überschreiben, die die Eigenschaften in der clientseitigen MSBuild-Projektdatei definieren.

    Definieren Sie anschließend zwei MSBuild-Ziele. Wir erweitern den MSBuild-Prozess und überschreiben vordefinierte Ziele:

    • BeforeCompile: Das Ziel besteht darin, den benutzerdefinierten Task aufzurufen, um die Klasse zu generieren und die zu kompilierende Klasse einzubeziehen. Tasks in diesem Ziel werden eingefügt, bevor die Kernkompilierung durchgeführt wird. Eingabe- und Ausgabefelder stehen im Zusammenhang mit inkrementellen Builds. Wenn alle Ausgabeelemente aktuell sind, überspringt MSBuild das Ziel. Mit diesem inkrementellen Build des Ziels kann die Leistung Ihrer Builds erheblich erhöht werden. Ein Element wird als aktuell betrachtet, wenn dessen Ausgabedatei genau so alt oder neuer als seine Eingabedatei oder -dateien ist.

    • AfterClean: Das Ziel besteht im Löschen der generierten Klassendatei, nachdem eine allgemeine Bereinigung erfolgt. Tasks in diesem Ziel werden eingefügt, nachdem die Kernfunktionen für Bereinigung aufgerufen wurden. Es wird eine Wiederholung des Codegenerierungsschritts erzwungen, wenn das Ziel für die erneute Erstellung ausgeführt wird.

Generieren des NuGet-Pakets

Zum Generieren des NuGet-Pakets können Sie Visual Studio verwenden (klicken Sie mit der rechten Maustaste auf den Projektknoten im Projektmappen-Explorer, und wählen Sie dann Packen aus). Sie können auch die Befehlszeile verwenden. Navigieren Sie zu dem Ordner, in dem sich die Taskprojektdatei AppSettingStronglyTyped.csproj befindet, und führen Sie den folgenden Befehl aus:

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

Herzlichen Glückwunsch! Sie haben ein NuGet-Paket namens \AppSettingStronglyTyped\AppSettingStronglyTyped\AppSettingStronglyTyped.1.0.0.nupkg generiert.

Das Paket weist die Erweiterung .nupkg auf und ist eine komprimierte ZIP-Datei. Sie können es mit einem ZIP-Tool öffnen. Die .target- und .props-Dateien befinden sich im Ordner build. Die .dll-Datei befindet sich im Ordner lib\netstandard2.0\. Die AppSettingStronglyTyped.nuspec-Datei befindet sich auf Stammebene.

(Optional) Unterstützung der Festlegung von Zielversionen

Sie sollten MSBuild-Verteilungen sowohl vom Typ Full (.NET Framework) als auch vom Typ Core (einschließlich .NET 5 und höher) unterstützen, um die breitestmögliche Benutzerbasis abzudecken.

Für normale .NET SDK-Projekte bedeutet die Festlegung von Zielversionen, dass mehrere TargetFrameworks in Ihrer Projektdatei festgelegt werden. In diesem Fall werden Builds für beide TargetFrameworkMonikers ausgelöst, und die Gesamtergebnisse können als ein einziges Artefakt gepackt werden.

Das ist im Hinblick auf MSBuild noch nicht alles. MSBuild wird in zwei Komponenten bereitgestellt: in Visual Studio und im .NET SDK. Dies sind sehr unterschiedliche Laufzeitumgebungen. Die eine wird auf der .NET Framework-Runtime und die andere auf der CoreCLR ausgeführt. Dies bedeutet, dass Ihr Code zwar auf netstandard2.0 ausgelegt sein kann, aber Ihre Aufgabenlogik möglicherweise Unterschiede aufweist, je nachdem, welcher MSBuild-Runtimetyp gerade verwendet wird. Da es so viele neue APIs in .NET 5.0 und höher gibt, ist es in der Praxis sinnvoll, sowohl den MSBuild-Aufgabenquellcode für mehrere TargetFrameworkMonikers als auch die MSBuild-Ziellogik für mehrere MSBuild-Runtimetypen festzulegen.

Für die Festlegung von Zielversionen erforderliche Änderungen

So legen Sie mehrere TargetFrameworkMonikers (TFM) als Ziel fest

  1. Ändern Sie Ihre Projektdatei so, dass die TFMs net472 und net6.0 verwendet werden (letzterer kann je nach der SDK-Ebene, die Sie als Ziel verwenden möchten, abweichen). Es empfiehlt sich, netcoreapp3.1 als Ziel zu verwenden, bis der Support für .NET Core 3.1 ausläuft. In diesem Fall ändert sich die Paketordnerstruktur von tasks/ in tasks/<TFM>/.

    <TargetFrameworks>net472;net6.0</TargetFrameworks>
    
  2. Aktualisieren Sie Ihre .targets-Dateien so, dass der richtige TFM zum Laden Ihrer Aufgaben verwendet wird. Der erforderliche TFM ändert sich basierend auf dem oben ausgewählten .NET-TFM, aber für ein auf net472 und net6.0 ausgerichtetes Projekt läge eine Eigenschaft ähnlich der folgenden vor:

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

Dieser Code verwendet die Eigenschaft MSBuildRuntimeType als Proxy für die aktive Hostingumgebung. Sobald diese Eigenschaft festgelegt ist, können Sie sie in UsingTask verwenden, um die richtige AssemblyFile zu laden:

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

Nächste Schritte

Viele Tasks umfassen das Aufrufen einer ausführbaren Datei. In einigen Szenarien können Sie den Exec-Task verwenden, aber wenn die Einschränkungen des Exec-Tasks ein Problem darstellen, können Sie auch einen benutzerdefinierten Task erstellen. Im folgenden Tutorial werden beide Optionen mit einem realistischeren Codegenerierungsszenario beschrieben: Erstellen eines benutzerdefinierten Tasks zum Generieren von Clientcode für eine REST-API.

Oder erfahren Sie, wie Sie einen benutzerdefinierten Tasks testen.