Freigeben über


Lernprogramm: Erstellen einer benutzerdefinierten Aufgabe für die Codegenerierung

In diesem Lernprogramm erstellen Sie eine benutzerdefinierte Aufgabe in MSBuild in C#, die die Codegenerierung behandelt, und dann verwenden Sie die Aufgabe in einem Build. In diesem Beispiel wird die Verwendung von MSBuild zum Behandeln der Bereinigungs- und Neuerstellungsvorgänge veranschaulicht. Das Beispiel zeigt auch, wie inkrementelle Build unterstützt wird, 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 außerdem die Verwendung von NuGet zum Packen der Aufgabe für die Verteilung, und das Lernprogramm enthält einen optionalen Schritt, um den BinLog-Viewer zu verwenden, um die Problembehandlung zu verbessern.

Voraussetzungen

Sie sollten über ein Verständnis von MSBuild-Konzepten wie Aufgaben, Zielen und Eigenschaften verfügen. Weitere Informationen finden Sie unter MSBuild-Konzepte.

In den Beispielen ist MSBuild erforderlich, die mit Visual Studio installiert ist, aber auch separat installiert werden kann. Siehe Herunterladen von MSBuild ohne Visual Studio.

Einführung in das Codebeispiel

Im Beispiel wird eine Eingabetextdatei verwendet, die werte enthält, die festgelegt werden sollen, und eine C#-Codedatei mit Code erstellt, der diese Werte erstellt. Dies ist zwar ein einfaches Beispiel, aber dieselben grundlegenden Techniken können auf komplexere Codegenerierungsszenarien angewendet werden.

In diesem Lernprogramm erstellen Sie eine benutzerdefinierte MSBuild-Aufgabe namens "AppSettingStronglyTyped". Die Aufgabe liest eine Reihe von Textdateien und jede Datei mit Zeilen mit dem folgenden Format:

propertyName:type:defaultValue

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

Der vollständige Beispielcode für dieses Lernprogramm findet sich unter benutzerdefinierte Aufgabe – Codegenerierung im .NET-Beispiel-Repository 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 vollständigem MSBuild (dem von Visual Studio verwendeten) und tragbarem MSBuild, dem in der .NET Core-Befehlszeile gebündelten.

  • Full MSBuild: Diese Version von MSBuild befindet sich in der Regel in Visual Studio. Wird unter .NET Framework ausgeführt. Visual Studio verwendet dies, wenn Sie Build auf Ihrer Lösung oder Ihrem Projekt ausführen. Diese Version ist auch in einer Befehlszeilenumgebung verfügbar, z. B. der Visual Studio Developer Command Prompt oder PowerShell.
  • .NET MSBuild: Diese Version von MSBuild wird in der .NET Core-Befehlszeile gebündelt. Sie wird auf .NET Core ausgeführt. Visual Studio ruft diese Version von MSBuild nicht direkt auf. Es unterstützt nur Projekte, die mit Microsoft.NET.Sdk erstellt werden.

Wenn Sie Code zwischen .NET Framework und einer anderen .NET-Implementierung wie .NET Core freigeben möchten, sollte Ihre Bibliothek auf .NET Standard 2.0ausgerichtet sein und in Visual Studio ausgeführt werden, das auf .NET Framework ausgeführt wird. .NET Framework unterstützt .NET Standard 2.1 nicht.

Wählen Sie die MSBuild-API-Version aus, auf die verwiesen werden soll.

Beim Kompilieren einer benutzerdefinierten Aufgabe sollten Sie auf die Version der MSBuild-API (Microsoft.Build.*) verweisen, die der Mindestversion von Visual Studio und/oder dem .NET SDK entspricht, das Sie unterstützen möchten. Um beispielsweise Benutzer in Visual Studio 2019 zu unterstützen, sollten Sie gegen MSBuild 16.11 kompilieren.

Erstellen des benutzerdefinierten MSBuild-Tasks AppSettingStronglyTyped

Der erste Schritt besteht darin, die benutzerdefinierte MSBuild-Aufgabe zu erstellen. Informationen zum Schreiben einer benutzerdefinierten MSBuild-Aufgabe helfen Ihnen möglicherweise, die folgenden Schritte zu verstehen. Eine benutzerdefinierte MSBuild-Aufgabe ist eine Klasse, die die ITask Schnittstelle implementiert.

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

  2. Fügen Sie drei Eigenschaften hinzu. Diese Eigenschaften definieren die Parameter des Vorgangs, den Benutzer beim Verwenden des Vorgangs in einem Clientprojekt festlegen:

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

    Die Aufgabe verarbeitet die SettingFiles und generiert die Klasse SettingNamespaceName.SettingClassName. Die generierte Klasse verfügt über eine Reihe von Konstanten basierend auf dem Inhalt der Textdatei.

    Die Aufgabenausgabe 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 eine benutzerdefinierte Aufgabe erstellen, erben Sie von Microsoft.Build.Utilities.Task. Um die Aufgabe zu implementieren, setzen Sie die Execute()-Methode außer Kraft. Die Execute-Methode gibt true zurück, wenn die Aufgabe erfolgreich ist, und andernfalls false. Task implementiert Microsoft.Build.Framework.ITask und stellt Standardimplementierungen einiger ITask Member bereit und stellt darüber hinaus einige Protokollierungsfunktionen bereit. Es ist wichtig, den Status im Protokoll auszugeben, um die Aufgabe zu diagnostizieren und zu beheben, insbesondere, wenn ein Problem auftritt und die Aufgabe ein Fehlerergebnis zurückgeben muss (false). Bei Fehler signalisiert die Klasse den Fehler durch Aufrufen von 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;
    }
    

    Die Aufgaben-API ermöglicht das Zurückgeben von "false", was den Fehler angibt, ohne dem Benutzer anzugeben, was nicht geklappt hat. Es ist am besten, !Log.HasLoggedErrors anstelle eines booleschen Codes zurückzugeben und einen Fehler zu protokollieren, wenn ein Fehler auftritt.

Protokollfehler

Die bewährte Methode beim Protokollieren von Fehlern besteht darin, Details wie die Zeilennummer und einen eindeutigen Fehlercode beim Protokollieren eines Fehlers bereitzustellen. 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 Ihrer Aufgabe abfangen, 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 angezeigt. siehe AppSettingStronglyTyped.cs im Beispiel-Repository.

Der Beispielcode generiert C#-Code während des Buildvorgangs. Die Aufgabe ist wie jede andere C#-Klasse. Wenn Sie mit diesem Lernprogramm fertig sind, können Sie sie anpassen und hinzufügen, welche Funktionen für Ihr eigenes Szenario erforderlich sind.

Erstellen Sie eine Konsolen-App und verwenden Sie die benutzerdefinierte Aufgabe.

In diesem Abschnitt erstellen Sie eine .NET Core-Konsolen-Standard-App, die die Aufgabe verwendet.

Wichtig

Es ist wichtig, das Generieren einer benutzerdefinierten MSBuild-Aufgabe im selben MSBuild-Prozess zu vermeiden, der sie nutzen wird. 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 Möglichkeit zum Verteilen einer Aufgabe ist ein NuGet-Paket, aber während der Entwicklung und beim Debuggen können Sie alle Informationen zu .props und .targets direkt in die Projektdatei Ihrer Anwendung einfügen und dann zum NuGet-Format wechseln, wenn Sie die Aufgabe an andere Personen verteilen.

  2. Ändern Sie die Projektdatei, um die Codegenerierungsaufgabe zu nutzen. Die Codeauflistung in diesem Abschnitt zeigt die geänderte Projektdatei nach dem Verweisen auf den Vorgang, das Festlegen der Eingabeparameter für den Vorgang und das Schreiben der Ziele für die Verarbeitung sauberer und neuerstellungsvorgänge, sodass die generierte Codedatei wie erwartet entfernt wird.

    Aufgaben werden mithilfe des UsingTask-Elements (MSBuild)registriert. Das UsingTask-Element registriert die Aufgabe; es teilt MSBuild den Namen der Aufgabe mit und wie es die Assembly finden und ausführen kann, die die Aufgabenklasse enthält. Der Assemblypfad ist relativ zur Projektdatei.

    Die PropertyGroup enthält die Eigenschaftendefinitionen, die den in der Aufgabe definierten Eigenschaften entsprechen. Diese Eigenschaften werden mithilfe von Attributen festgelegt, und der Aufgabenname 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.

    Um die Aufgabe aufzurufen, fügen Sie die Aufgabe dem entsprechenden Ziel hinzu, in diesem Fall GenerateSetting.

    Das Ziel ForceGenerateOnRebuild behandelt 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>
    

    Anmerkung

    Anstatt ein Ziel wie CoreCleanaußer Kraft zu setzen, verwendet dieser Code eine andere Möglichkeit, um die Ziele (BeforeTarget und AfterTarget)zu ordnen. SDK-Formatprojekte haben einen impliziten Import von Zielen nach der letzten Zeile der Projektdatei; Dies bedeutet, dass Sie die Standardziele nicht außer Kraft setzen können, es sei denn, Sie geben Die Importe manuell an. Siehe Außerkraftsetzen vordefinierter Ziele.

    Die Attribute Inputs und Outputs machen MSBuild effizienter, indem sie Informationen für inkrementelle Builds bereitstellen. Die Datumswerte der Eingaben werden mit den Ausgaben verglichen, um festzustellen, ob das Ziel ausgeführt werden muss oder ob die Ausgabe des vorherigen Builds wiederverwendet werden kann.

  3. Erstellen Sie die Eingabetextdatei mit der zu ermittelnden Erweiterung. Erstellen Sie mithilfe der Standarderweiterung MyValues.mysettings im Stammverzeichnis mit folgendem Inhalt:

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

  5. Die Klasse MySetting befindet sich im falschen Namespace. Nehmen Sie daher 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. Erstellen Sie es 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) neu definieren, die als Eingabe verwendet werden sollen, und den Speicherort (RootFolder) davon, wenn Sie möchten.

  7. Öffnen Sie Program.cs, und ändern Sie das hartcodierte „Hallo Welt!!“ in die benutzerdefinierte Konstante:

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

Führen Sie das Programm aus; sie druckt die Begrüßung aus der generierten Klasse.

(Optional) Protokollieren von Ereignissen während des Buildprozesses

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

# 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 MSBuild Binary und Structured Log Viewergeöffnet werden kann. Die Option /t:rebuild bedeutet, dass das Neuerstellungsziel ausgeführt werden soll. Sie wird die Regeneration der generierten Codedatei erzwingen.

Glückwunsch! Sie haben eine Aufgabe erstellt, die Code generiert und in einem Build verwendet.

Aufgabe für die Verteilung verpacken

Wenn Sie Ihre benutzerdefinierte Aufgabe nur in einigen Projekten oder in einer einzigen Lösung verwenden müssen, ist die Verwendung der Aufgabe als unformatierte Assembly möglicherweise alles, was Sie benötigen, aber die beste Möglichkeit, Ihre Aufgabe für die Verwendung an anderer Stelle vorzubereiten oder sie für andere personen freizugeben, ist als NuGet-Paket.

MSBuild-Aufgabenpakete weisen einige wichtige Unterschiede von Bibliotheks-NuGet-Paketen 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 einen lib/<target framework> Ordner, da NuGet die Assemblys in jedes Paket einschließen würde, das die Aufgabe verwendet.
  • Sie müssen nur mit den Microsoft.Build-Assemblys kompiliert werden – zur Runtime 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, mit der MSBuild die Abhängigkeiten der Aufgabe (insbesondere systemeigene Abhängigkeiten) einheitlich laden kann.

Um alle diese Ziele zu erreichen, müssen Sie einige Änderungen an der Standardprojektdatei über und darüber hinaus vornehmen, mit denen Sie möglicherweise vertraut sind.

Erstellen eines NuGet-Pakets

Das Erstellen eines NuGet-Pakets ist die empfohlene Methode zum Verteilen Ihrer benutzerdefinierten Aufgabe an andere Benutzer.

Vorbereiten der Generierung des Pakets

Um das Generieren eines NuGet-Pakets vorzubereiten, nehmen Sie einige Änderungen an der Projektdatei vor, um die Details anzugeben, die das Paket beschreiben. Die erstellte Anfangsprojektdatei ä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. Eine vollständige Liste der unterstützten MSBuild-Eigenschaften finden Sie in der Pack-Dokumentation:

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

Die Eigenschaft CopyLocalLockFileAssemblies ist erforderlich, um sicherzustellen, dass Abhängigkeiten in das Ausgabeverzeichnis kopiert werden.

Kennzeichnen von Abhängigkeiten als privat

Die Abhängigkeiten Ihrer MSBuild-Aufgabe 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. Das erfordert zwei Schritte: Markieren Sie Ihre Assemblys als privat und betten Sie sie tatsächlich in das generierte Paket ein. In diesem Beispiel nehmen wir an, dass Ihre Aufgabe von Microsoft.Extensions.DependencyInjection abhängt, um zu funktionieren. Fügen Sie also ein PackageReference zu Microsoft.Extensions.DependencyInjection in Version 6.0.0hinzu.

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

Abhängigkeiten in das Paket bündeln

Sie müssen auch die Laufzeitressourcen unserer Abhängigkeiten in das Aufgabenpaket einbetten. Hierfür gibt es zwei Teile: 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 Prozess 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>
    <!-- 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>

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

Wie oben beschrieben, wird diese Abhängigkeit von MSBuild selbst zur Laufzeit bereitgestellt, daher müssen wir sie nicht in das Paket bündeln. 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 die 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>

Einschließen von MSBuild-Eigenschaften und Zielen in ein Paket

Um Hintergrundinformationen zu diesem Abschnitt zu erhalten, lesen Sie über -Eigenschaften und -Ziele und anschließend, wie Sie Eigenschaften und Ziele in ein NuGet-Paket einschließenkönnen.

In einigen Fällen möchten Sie möglicherweise benutzerdefinierte Buildziele oder Eigenschaften in Projekten hinzufügen, die Ihr Paket nutzen, z. B. das Ausführen eines benutzerdefinierten Tools oder Prozesses während des Builds. Dazu platzieren Sie Dateien im Formular <package_id>.targets oder <package_id>.props im Ordner build im Projekt.

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

In diesem Abschnitt richten Sie die Implementierung der Aufgaben in den Dateien .props und .targets ein, die in unserem NuGet-Paket enthalten sind und automatisch aus einem referenzierenden Projekt geladen werden können.

  1. Fügen Sie in der Projektdatei des Vorgangs AppSettingStronglyTyped.csprojden 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 früh in Microsoft.Common.propsimportiert, und die später definierten Eigenschaften sind nicht verfügbar. Vermeiden Sie daher, auf Eigenschaften zu verweisen, 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. Es kann also Eigenschaften und Ziele überschreiben, die in der meisten Buildlogik definiert sind, oder für alle eure Projekte Eigenschaften festlegen, unabhängig davon, welche Eigenschaften die einzelnen Projekte festlegen. Weitere Informationen finden Sie unter Importreihenfolge.

    AppSettingStronglyTyped.props enthält die Aufgabe 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 AppSettingStronglyTyped.props Datei wird automatisch eingeschlossen, wenn das Paket installiert ist. Anschließend steht dem Client die Aufgabe und einige Standardwerte zur Verfügung. Es wird jedoch nie verwendet. Um diesen Code in Aktion zu setzen, definieren Sie einige Ziele in der AppSettingStronglyTyped.targets Datei, die auch automatisch eingeschlossen werden, wenn das Paket installiert 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 Textdateien darstellt (es können mehr als eine sein), die gelesen werden sollen und die Teil der Aufgabenparameter sind. Es gibt Standardwerte für den Speicherort und die Erweiterung, nach der wir suchen, aber Sie können die Werte überschreiben, die die Eigenschaften in der Client-MSBuild-Projektdatei definieren.

    Definieren Sie dann zwei MSBuild-Ziele. Sie erweitern den MSBuild-Prozess und überschreiben vordefinierte Ziele:

    • BeforeCompile: Ziel ist es, die benutzerdefinierte Aufgabe aufzurufen, um die Klasse zu generieren und die zu kompilierende Klasse einzuschließen. Aufgaben 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. Dieser inkrementelle Build des Zielobjekts kann die Leistung Ihrer Builds erheblich verbessern. Ein Element wird als up-to-datum betrachtet, wenn die Ausgabedatei dasselbe Alter oder neuer ist als die Eingabedatei oder Dateien.

    • AfterClean: Das Ziel besteht darin, die generierte Klassendatei nach einer allgemeinen Bereinigung zu löschen. Tasks in diesem Ziel werden eingefügt, nachdem die Kernfunktionen für Bereinigung aufgerufen wurden. Er erzwingt, dass der Codegenerierungsschritt wiederholt wird, wenn das Neuerstellungsziel ausgeführt wird.

Generieren des NuGet-Pakets

Um das NuGet-Paket zu generieren, können Sie Visual Studio verwenden (klicken Sie mit der rechten Maustaste auf den Projektknoten im Projektmappen-Explorer, und wählen Sie Pack-). Sie können dies auch über die Befehlszeile tun. Navigieren Sie zu dem Ordner, in dem die Projektdatei AppSettingStronglyTyped.csproj vorhanden ist, und führen Sie den folgenden Befehl aus:

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

Glückwunsch! Sie haben ein NuGet-Paket namens \AppSettingStronglyTyped\AppSettingStronglyTyped\AppSettingStronglyTyped.1.0.0.nupkggeneriert.

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

(Optional) Unterstützen von Multitargeting

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

Bei "normalen" .NET SDK-Projekten bedeutet Multitargeting, dass mehrere TargetFrameworks in Ihrer Projektdatei festgelegt werden. Wenn Sie dies tun, werden Builds für die beiden TargetFrameworkMonikers ausgelöst, und die Ergebnisse können als ein einziges Artefakt zusammengefasst werden.

Das ist nicht die ganze Geschichte für MSBuild. MSBuild wird in zwei Komponenten bereitgestellt: in Visual Studio und im .NET SDK. Dies sind sehr unterschiedliche Laufzeitumgebungen; eine wird auf der .NET Framework-Laufzeit und andere auf der CoreCLR ausgeführt. Dies bedeutet, dass Ihr Code zwar auf netstandard2.0 abzielen kann, ihre Aufgabenlogik möglicherweise Unterschiede auf der Grundlage des derzeit verwendeten MSBuild-Laufzeittyps aufweist. Da es so viele neue APIs in .NET 5.0 und höher gibt, ist es in der Praxis sinnvoll, sowohl den MSBuild-Taskquellcode 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. Bitte ändern Sie Ihre Projektdatei so, dass sie die net472- und net6.0-TFMs verwendet (letzteres kann sich je nach Ziel-SDK-Ebene ändern). Möglicherweise möchten Sie netcoreapp3.1 festlegen, bis .NET Core 3.1 nicht mehr unterstützt wird. 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 Tasks verwendet wird. Der erforderliche TFM ändert sich basierend auf dem oben ausgewählten .NET-TFM, aber für ein Projekt mit net472 und net6.0 als Ziel 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. Nachdem diese Eigenschaft festgelegt wurde, können Sie sie in der UsingTask verwenden, um die richtige AssemblyFilezu laden:

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

Nächste Schritte

Viele Aufgaben umfassen das Aufrufen einer ausführbaren Datei. In einigen Szenarien können Sie die Exec-Aufgabe verwenden, aber wenn die Einschränkungen der Exec-Aufgabe ein Problem sind, können Sie auch eine benutzerdefinierte Aufgabe erstellen. Im folgenden Lernprogramm werden beide Optionen mit einem realistischeren Szenario der Codegenerierung erläutert: Erstellen einer benutzerdefinierten Aufgabe zum Generieren von Clientcode für eine REST-API.

Oder erfahren Sie, wie Sie eine benutzerdefinierte Aufgabe testen.