Udostępnij za pośrednictwem


Samouczek: tworzenie niestandardowego zadania na potrzeby generowania kodu

W tym samouczku utworzysz zadanie niestandardowe w programie MSBuild w języku C#, które obsługuje generowanie kodu, a następnie użyjesz zadania w kompilacji. W tym przykładzie pokazano, jak używać programu MSBuild do obsługi operacji czyszczenia i ponownego kompilowania. W przykładzie pokazano również, jak obsługiwać kompilację przyrostową, tak aby kod był generowany tylko wtedy, gdy pliki wejściowe uległy zmianie. Przedstawione techniki mają zastosowanie do szerokiego zakresu scenariuszy generowania kodu. Kroki pokazują również użycie narzędzia NuGet do spakowania zadania do dystrybucji, a samouczek zawiera opcjonalny krok umożliwiający korzystanie z przeglądarki BinLog w celu ulepszenia środowiska rozwiązywania problemów.

Warunki wstępne

Należy poznać pojęcia dotyczące programu MSBuild, takie jak zadania, obiekty docelowe i właściwości. Zobacz pojęcia dotyczące programu MSBuild.

Przykłady wymagają programu MSBuild zainstalowanego w programie Visual Studio, ale można go również zainstalować oddzielnie. Zobacz Pobierz program MSBuild bez programu Visual Studio.

Wprowadzenie do przykładu kodu

W przykładzie jest pobierany wejściowy plik tekstowy zawierający wartości do ustawienia i tworzy plik kodu C# z kodem, który tworzy te wartości. Chociaż jest to prosty przykład, te same podstawowe techniki można zastosować do bardziej złożonych scenariuszy generowania kodu.

W tym samouczku utworzysz niestandardowe zadanie MSBuild o nazwie AppSettingStronglyTyped. Zadanie będzie polegało na odczytywaniu zestawu plików tekstowych, z których każdy zawiera wiersze o następującym formacie:

propertyName:type:defaultValue

Kod generuje klasę języka C# ze wszystkimi stałymi. Problem powinien zatrzymać kompilację i przekazać użytkownikowi wystarczające informacje, aby zdiagnozować problem.

Kompletny przykładowy kod dla tego samouczka znajduje się w Niestandardowe zadanie — generowanie kodu w repozytorium przykładów platformy .NET w witrynie GitHub.

Tworzenie projektu AppSettingStronglyTyped

Utwórz bibliotekę klas .NET Standard. Platforma powinna być zgodna z .NET Standard 2.0.

Zwróć uwagę na różnicę między pełnym programem MSBuild (używanym przez program Visual Studio) i przenośnym programem MSBuild, który jest dołączony do wiersza polecenia platformy .NET Core.

  • Pełny program MSBuild: ta wersja programu MSBuild zwykle znajduje się w programie Visual Studio. Działa w programie .NET Framework. Program Visual Studio używa tego polecenia podczas wykonywania Build w rozwiązaniu lub projekcie. Ta wersja jest również dostępna w środowisku wiersza polecenia, takim jak wiersz polecenia dla deweloperów programu Visual Studio lub program PowerShell.
  • .NET MSBuild: ta wersja programu MSBuild jest dołączona w wierszu polecenia platformy .NET Core. Działa on na platformie .NET Core. Program Visual Studio nie wywołuje bezpośrednio tej wersji programu MSBuild. Obsługuje tylko projekty kompilujące się przy użyciu zestawu Microsoft.NET.Sdk.

Jeśli chcesz udostępnić kod między programem .NET Framework i inną implementacją platformy .NET, taką jak .NET Core, biblioteka powinna być docelowa .NET Standard 2.0i chcesz uruchomić ją w programie Visual Studio, który działa na platformie .NET Framework. Program .NET Framework nie obsługuje platformy .NET Standard 2.1.

Wybierz wersję interfejsu API MSBuild do użycia

Podczas kompilowania zadania niestandardowego należy odwołać się do wersji interfejsu API MSBuild (Microsoft.Build.*), która jest zgodna z minimalną wersją programu Visual Studio i/lub zestawu SDK platformy .NET, którego oczekujesz. Aby na przykład obsługiwać użytkowników programu Visual Studio 2019, należy skompilować program MSBuild 16.11.

Utwórz niestandardowe zadanie MSBuild AppSettingStronglyTyped

Pierwszym krokiem jest utworzenie niestandardowego zadania MSBuild. Informacje o tym, jak napisać niestandardowe zadanie MSBuild, mogą ułatwić zrozumienie poniższych kroków. Niestandardowe zadanie programu MSBuild to klasa, która implementuje interfejs ITask.

  1. Dodaj odwołanie do pakietu NuGet Microsoft.Build.Utilities. Core, a następnie utwórz klasę o nazwie AppSettingStronglyTyped pochodną Microsoft.Build.Utilities.Task.

  2. Dodaj trzy właściwości. Te właściwości definiują parametry zadania ustawionego przez użytkowników podczas korzystania z zadania w projekcie klienta:

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

    Zadanie przetwarza SettingFiles i generuje klasę SettingNamespaceName.SettingClassName. Wygenerowana klasa będzie zawierać zestaw stałych na podstawie zawartości pliku tekstowego.

    Dane wyjściowe zadania powinny być ciągiem, który daje nazwę pliku wygenerowanego kodu:

    // The filename where the class was generated
    [Output]
    public string ClassNameFile { get; set; }
    
  3. Podczas tworzenia zadania niestandardowego dziedziczysz z Microsoft.Build.Utilities.Task. Aby zaimplementować zadanie, należy zastąpić metodę Execute(). Metoda Execute zwraca true, jeśli zadanie zakończy się pomyślnie, a false w przeciwnym razie. Task implementuje Microsoft.Build.Framework.ITask i zapewnia domyślne implementacje niektórych elementów członkowskich ITask, a ponadto zapewnia pewną funkcjonalność logowania. Ważne jest, aby zapisywać stan do logu, aby zdiagnozować i rozwiązać problemy z zadaniem, zwłaszcza jeśli coś pójdzie nie tak i zadanie musi zwrócić wynik błędu (false). Po błędzie klasa sygnalizuje błąd, wywołując 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;
    }
    

    Interfejs API zadań umożliwia zwracanie wartości false, wskazując błąd bez wskazania użytkownikowi, co poszło nie tak. Najlepiej jest zwrócić !Log.HasLoggedErrors zamiast kodu logicznego i zarejestrować błąd, gdy coś pójdzie nie tak.

Błędy logu

Najlepszym rozwiązaniem podczas rejestrowania błędów jest podanie szczegółów, takich jak numer wiersza i odrębny kod błędu podczas rejestrowania błędu. Poniższy kod analizuje plik wejściowy tekstowy i używa metody TaskLoggingHelper.LogError z numerem wiersza w pliku tekstowym, który wygenerował błąd.

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

Korzystając z technik przedstawionych w poprzednim kodzie, błędy w składni pliku wejściowego tekstu są wyświetlane jako błędy kompilacji z przydatnymi informacjami diagnostycznymi:

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)

Podczas przechwytywania wyjątków w zadaniu użyj metody TaskLoggingHelper.LogErrorFromException. Spowoduje to poprawienie danych wyjściowych błędu, na przykład przez uzyskanie stosu wywołań, w którym zgłoszono wyjątek.

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

Implementacja innych metod, które używają tych danych wejściowych do skompilowania tekstu dla wygenerowanego pliku kodu, nie jest tutaj pokazana; zobacz AppSettingStronglyTyped.cs w repozytorium przykładowym.

Przykładowy kod generuje kod języka C# podczas procesu kompilacji. Zadanie jest podobne do każdej innej klasy języka C#, więc po zakończeniu pracy z tym samouczkiem możesz go dostosować i dodać dowolną funkcjonalność do własnego scenariusza.

Wygeneruj aplikację konsolową i użyj zadania niestandardowego.

W tej sekcji utworzysz standardową aplikację konsolową platformy .NET Core, która używa zadania.

Ważny

Ważne jest, aby uniknąć generowania niestandardowego zadania MSBuild w tym samym procesie MSBuild, który będzie z niego korzystać. Nowy projekt powinien znajdować się w całkowicie innym rozwiązaniu w programie Visual Studio, albo nowy projekt używa wcześniej wygenerowanej biblioteki DLL i przeniesionej z miejsca standardowego wyjścia.

  1. Utwórz projekt konsoli .NET MSBuildConsoleExample w nowym rozwiązaniu programu Visual Studio.

    Normalnym sposobem dystrybucji zadania jest pakiet NuGet, ale podczas programowania i debugowania można uwzględnić wszystkie informacje na temat .props i .targets bezpośrednio w pliku projektu aplikacji, a następnie przejść do formatu NuGet podczas dystrybucji zadania do innych osób.

  2. Zmodyfikuj plik projektu, aby korzystać z zadania generowania kodu. Lista kodu w tej sekcji przedstawia zmodyfikowany plik projektu po odwołaniu się do zadania, ustawieniu parametrów wejściowych dla zadania i zapisaniu obiektów docelowych do obsługi operacji czystego i ponownego kompilowania, tak aby wygenerowany plik kodu został usunięty zgodnie z oczekiwaniami.

    Zadania są rejestrowane przy użyciu elementu UsingTask (MSBuild). Element UsingTask rejestruje zadanie; informuje MSBuild nazwę zadania oraz sposób lokalizowania i uruchamiania zestawu zawierającego klasę zadań. Ścieżka zestawu jest względna względem pliku projektu.

    PropertyGroup zawiera definicje właściwości, które odpowiadają właściwościom zdefiniowanym w zadaniu. Te właściwości są ustawiane przy użyciu atrybutów, a nazwa zadania jest używana jako nazwa elementu.

    TaskName jest nazwą zadania do odniesienia w zestawie. Ten atrybut powinien zawsze używać w pełni określonych przestrzeni nazw. AssemblyFile to ścieżka pliku dla zestawu.

    Aby wywołać zadanie, dodaj zadanie do odpowiedniego obiektu docelowego, w tym przypadku GenerateSetting.

    Docelowy ForceGenerateOnRebuild wykonuje operacje czyszczenia i odbudowy poprzez usunięcie wygenerowanego pliku. Jest ona ustawiona tak, aby była uruchamiana po obiekcie docelowym CoreClean przez ustawienie atrybutu AfterTargets na wartość 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>
    

    Notatka

    Zamiast zastępować obiekt docelowy, taki jak CoreClean, ten kod używa innego sposobu porządkowania obiektów docelowych (BeforeTarget i AfterTarget). Projekty w stylu zestawu SDK mają niejawny import elementów docelowych po ostatnim wierszu pliku projektu; Oznacza to, że nie można zastąpić domyślnych obiektów docelowych, chyba że ręcznie określisz importy. Zobacz Zastępowanie wstępnie zdefiniowanych celów.

    Atrybuty Inputs i Outputs pomagają programowi MSBuild zwiększyć wydajność, dostarczając informacje na temat kompilacji przyrostowych. Daty danych wejściowych są porównywane z danymi wyjściowymi, aby sprawdzić, czy element docelowy musi zostać uruchomiony, czy dane wyjściowe poprzedniej kompilacji mogą być ponownie używane.

  3. Utwórz wejściowy plik tekstowy z rozszerzeniem zdefiniowanym do odnalezienia. Używając rozszerzenia domyślnego, utwórz MyValues.mysettings w katalogu głównym z następującą zawartością:

    Greeting:string:Hello World!
    
  4. Skompiluj ponownie, a wygenerowany plik powinien zostać utworzony i skompilowany. Sprawdź folder projektu dla pliku MySetting.generated.cs.

  5. Klasa MySetting znajduje się w niewłaściwej przestrzeni nazw, więc teraz wprowadź zmianę w celu korzystania z przestrzeni nazw aplikacji. Otwórz plik projektu i dodaj następujący kod:

    <PropertyGroup>
        <SettingNamespace>MSBuildConsoleExample</SettingNamespace>
    </PropertyGroup>
    
  6. Zbuduj ponownie i zwróć uwagę, że klasa znajduje się w przestrzeni nazw MSBuildConsoleExample. W ten sposób można ponownie zdefiniować wygenerowaną nazwę klasy (SettingClass), pliki rozszerzenia tekstowego (SettingExtensionFile) do użycia jako dane wejściowe i lokalizację (RootFolder) z nich, jeśli chcesz.

  7. Otwórz Program.cs i zmień zakodowany na stałe kod "Hello World!". do stałej zdefiniowanej przez użytkownika:

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

Wykonaj program; spowoduje wydrukowanie powitania z wygenerowanej klasy.

(Opcjonalnie) Rejestrowanie zdarzeń podczas procesu kompilacji

Można skompilować przy użyciu polecenia w wierszu polecenia. Przejdź do folderu projektu. Użyjesz opcji -bl (binary log), aby wygenerować dziennik binarny. Dziennik binarny będzie miał przydatne informacje, aby wiedzieć, co się dzieje podczas procesu kompilacji.

# 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

Oba polecenia generują plik dziennika msbuild.binlog, który można otworzyć za pomocą msBuild Binary i Structured Log Viewer. Opcja /t:rebuild oznacza uruchomienie celu przebudowy. Wymusi ponowne generowanie wygenerowanego pliku kodu.

Gratulacje! Utworzono zadanie, które generuje kod i używało go w kompilacji.

Spakuj zadanie dystrybucji

Jeśli musisz korzystać z niestandardowego zadania tylko w kilku projektach lub jednym rozwiązaniu, użycie zadania jako pierwotnego zestawu (assembly) może być wystarczające. Jednak najlepszym sposobem na przygotowanie zadania do użycia w innym miejscu lub do jego udostępnienia innym jest stworzenie paczki NuGet.

Pakiety zadań MSBuild różnią się kluczowymi cechami od pakietów biblioteki NuGet.

  • Muszą one zintegrować własne zależności zestawów zamiast udostępniać te zależności w projekcie odbiorczym
  • Nie pakują żadnych wymaganych zestawów do folderu lib/<target framework>, ponieważ spowodowałoby to, że NuGet dołączy te zestawy do każdego pakietu, który korzysta z tego zadania.
  • Muszą one tylko skompilować z wykorzystaniem zestawów Microsoft.Build — w czasie wykonywania będą one dostarczane przez rzeczywisty silnik MSBuild, więc nie muszą być uwzględnione w pakiecie.
  • Generują one specjalny plik .deps.json, który pomaga programowi MSBuild załadować zależności zadania (zwłaszcza zależności natywne) w spójny sposób

Aby osiągnąć wszystkie te cele, musisz wprowadzić kilka zmian w standardowym pliku projektu, które wykraczają poza te, które możesz już znać.

Tworzenie pakietu NuGet

Tworzenie pakietu NuGet jest zalecanym sposobem dystrybuowania niestandardowego zadania do innych osób.

Przygotowanie do wygenerowania pakietu

Aby przygotować się do wygenerowania pakietu NuGet, wprowadź pewne zmiany w pliku projektu, aby określić szczegóły opisane w pakiecie. Utworzony początkowy plik projektu przypomina następujący kod:

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

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

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

</Project>

Aby wygenerować pakiet NuGet, dodaj następujący kod, aby ustawić właściwości pakietu. Pełną listę obsługiwanych właściwości programu MSBuild można znaleźć w dokumentacji pakietu 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>

Właściwość CopyLocalLockFileAssemblies jest wymagana, aby upewnić się, że zależności są kopiowane do katalogu wyjściowego.

Oznacz zależności jako prywatne

Zależności zadania MSBuild muszą być spakowane wewnątrz pakietu; nie można ich wyrazić jako normalnych odwołań do pakietu. Pakiet nie uwidacznia żadnych regularnych zależności użytkownikom zewnętrznym. W tym celu należy wykonać dwa kroki: oznaczenie swoich zestawów jako prywatnych i faktyczne osadzenie ich w wygenerowanym pakiecie. W tym przykładzie załóżmy, że Twoje zadanie zależy od Microsoft.Extensions.DependencyInjection, więc dodaj PackageReference do Microsoft.Extensions.DependencyInjection w wersji 6.0.0.

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

Teraz oznacz każdą zależność tego projektu zadania, zarówno PackageReference, jak i ProjectReference za pomocą atrybutu PrivateAssets="all". Spowoduje to, że NuGet nie udostępniał tych zależności projektom używającym. Więcej informacji na temat kontrolowania zasobów zależności można uzyskać w dokumentacji narzędzia 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>

Włączenie zależności do pakietu

Należy również osadzić zasoby środowiska uruchomieniowego naszych zależności w pakiecie Zadań. Istnieją dwie części: element docelowy MSBuild, który dodaje nasze wymagania do grupy elementów BuildOutputInPackage, oraz kilka właściwości kontrolujących układ tych elementów BuildOutputInPackage. Można dowiedzieć się więcej o tym procesie w dokumentacji 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>

Nie należy dołączać zestawu Microsoft.Build.Utilities.Core

Jak wspomniano powyżej, ta zależność zostanie zapewniona przez program MSBuild w czasie wykonywania, więc nie musimy jej dołączać do pakietu. W tym celu dodaj atrybut ExcludeAssets="Runtime" do PackageReference.

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

Generowanie i osadzanie pliku deps.json

Plik deps.json może być używany przez program MSBuild, aby upewnić się, że załadowano poprawne wersje zależności. Należy dodać niektóre właściwości programu MSBuild, aby spowodować wygenerowanie pliku, ponieważ nie jest on domyślnie generowany dla bibliotek. Następnie dodaj element docelowy, aby uwzględnić go w danych wyjściowych pakietu, podobnie jak w przypadku zależności pakietów.

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

Uwzględnij właściwości i obiekty docelowe programu MSBuild w pakiecie

Aby zapoznać się z informacjami w tej sekcji, przeczytaj o właściwościach i obiektach docelowych, a następnie o tym, jak uwzględnić właściwości i obiekty docelowe w pakiecie NuGet.

W niektórych przypadkach możesz dodać niestandardowe obiekty docelowe kompilacji lub właściwości w projektach korzystających z pakietu, takich jak uruchamianie niestandardowego narzędzia lub procesu podczas kompilacji. W tym celu należy umieścić pliki w formularzu <package_id>.targets lub <package_id>.props w folderze build w projekcie.

Pliki w folderze głównym projektu kompilacji są uznawane za odpowiednie dla wszystkich platform docelowych.

W tej sekcji połączysz implementację zadań w plikach .props i .targets, które zostaną uwzględnione w naszym pakiecie NuGet i będą automatycznie ładowane z odwołującego się projektu.

  1. W pliku projektu zadania AppSettingStronglyTyped.csprojdodaj następujący kod:

    <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. Utwórz folder kompilacji i w tym folderze dodaj dwa pliki tekstowe: AppSettingStronglyTyped.props i AppSettingStronglyTyped.targets. AppSettingStronglyTyped.props jest importowany na początku w pliku Microsoft.Common.props, co sprawia, że właściwości zdefiniowane później w dokumencie są dla niego niedostępne. Dlatego unikaj odwoływania się do właściwości, które nie są jeszcze zdefiniowane; będą oceniać, że są puste.

    Directory.Build.targets jest importowany z Microsoft.Common.targets po zaimportowaniu plików .targets z pakietów NuGet. Dlatego może zastąpić właściwości i cele zdefiniowane w większości logiki budowania lub ustawić właściwości dla wszystkich projektów, niezależnie od tego, co ustawiają poszczególne projekty. Zobacz zamówienie importowe.

    AppSettingStronglyTyped.props zawiera zadanie i definiuje niektóre właściwości z wartościami domyślnymi:

    <?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. Plik AppSettingStronglyTyped.props jest automatycznie dołączany po zainstalowaniu pakietu. Następnie klient ma dostępne zadanie i niektóre wartości domyślne. Jednak nigdy nie jest używany. Aby umieścić ten kod w działaniu, zdefiniuj niektóre elementy docelowe w pliku AppSettingStronglyTyped.targets, który również zostanie automatycznie uwzględniony podczas instalowania pakietu:

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

    Pierwszym krokiem jest utworzenie ItemGroup, który reprezentuje pliki tekstowe (może to być więcej niż jeden) do odczytania i będzie to część naszego parametru zadania. Istnieją wartości domyślne lokalizacji i rozszerzenia, w którym szukamy, ale można zastąpić wartości definiujące właściwości w pliku projektu MSBuild klienta.

    Następnie zdefiniuj dwa obiekty docelowe MSBuild. rozszerzyć proces MSBuild, przesłaniając wstępnie zdefiniowane cele:

    • BeforeCompile: Celem jest wywołanie niestandardowego zadania w celu wygenerowania klasy, a następnie dołączenie tej klasy do procesu kompilacji. Zadania w tym obiekcie docelowym są wstawiane przed wykonaniem kompilacji podstawowej. Pola wejściowe i wyjściowe są powiązane z buildem przyrostowym . Jeśli wszystkie elementy wyjściowe są up-to-date, program MSBuild pomija element docelowy. Ta przyrostowa kompilacja projektu może znacznie poprawić wydajność twoich kompilacji. Element jest uznawany za up-to-date, jeśli jego plik wyjściowy jest w tym samym wieku lub nowszy niż jego plik wejściowy lub pliki.

    • AfterClean: Celem jest usunięcie wygenerowanego pliku klasy po zakończeniu ogólnego czyszczenia. Zadania w tym obiekcie docelowym są wstawiane po wywołaniu podstawowej funkcji czyszczenia. Wymusza ponowne powtórzenie kroku generowania kodu przy wykonaniu celu Rebuild.

Generowanie pakietu NuGet

Aby wygenerować pakiet NuGet, możesz użyć programu Visual Studio (kliknij prawym przyciskiem myszy węzeł projektu w eksploratorze rozwiązań i wybierz pozycję Pack). Można to również zrobić przy użyciu wiersza polecenia. Przejdź do folderu, w którym znajduje się plik projektu zadań AppSettingStronglyTyped.csproj, i wykonaj następujące polecenie:

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

Gratulacje! Wygenerowano pakiet NuGet o nazwie \AppSettingStronglyTyped\AppSettingStronglyTyped\AppSettingStronglyTyped.1.0.0.nupkg.

Pakiet ma rozszerzenie .nupkg i jest skompresowanym plikiem zip. Można go otworzyć za pomocą narzędzia zip. Pliki .target i .props znajdują się w folderze build. Plik .dll znajduje się w folderze lib\netstandard2.0\. Plik AppSettingStronglyTyped.nuspec znajduje się na poziomie głównym.

(Opcjonalnie) Obsługa wielotargetowania

Należy rozważyć obsługę dystrybucji msBuild Full (.NET Framework) i Core (w tym .NET 5 i nowszych), aby obsługiwać najszerszą możliwą bazę użytkowników.

W przypadku "normalnych" projektów zestawu .NET SDK wielotargeting oznacza ustawienie wielu elementów TargetFrameworks w pliku projektu. Wykonując to, kompilacje zostaną wykonane dla obu TargetFrameworkMonikers, a łączny wynik można zapakować jako pojedynczy artefakt.

To nie jest pełna historia programu MSBuild. MSBuild ma dwa podstawowe sposoby dystrybucji: Visual Studio i .NET SDK. Są to bardzo różne środowiska uruchomieniowe; jeden działa w środowisku uruchomieniowym programu .NET Framework, a drugi działa w rdzeniu CoreCLR. Oznacza to, że podczas gdy kod może być przeznaczony dla netstandard2.0, logika zadań może mieć różnice w zależności od typu środowiska uruchomieniowego MSBuild, który jest obecnie używany. Praktycznie rzecz biorąc, ponieważ w .NET 5.0 i nowszych wersjach istnieje wiele nowych interfejsów API, sensowne jest zarówno wielokrotne celowanie kodu źródłowego zadania MSBuild na różne TargetFrameworkMonikers, jak i wielokrotne celowanie logiki docelowej MSBuild na różne typy środowisk uruchomieniowych MSBuild.

Zmiany wymagane do wielotargetu

Aby obsługiwać wiele elementów TargetFrameworkMonikers (TFM):

  1. Zmień plik projektu, aby używał net472 i net6.0 TFMs (ten ostatni może ulec zmianie w zależności od poziomu zestawu SDK, który chcesz docelować). Warto ukierunkować się na netcoreapp3.1 do momentu, aż .NET Core 3.1 przestanie być wspierany. W takim przypadku struktura folderu pakietu zmienia się z tasks/ na tasks/<TFM>/.

    <TargetFrameworks>net472;net6.0</TargetFrameworks>
    
  2. Zaktualizuj pliki .targets, aby używać poprawnego TFM do ładowania swoich zadań. Wymagany TFM zmieni się w zależności od wybranego powyżej .NET TFM, ale dla projektu przeznaczonego na net472 i net6.0, właściwość będzie wyglądać następująco:

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

Ten kod używa właściwości MSBuildRuntimeType jako serwera proxy dla aktywnego środowiska hostingu. Po ustawieniu tej właściwości można jej użyć w UsingTask, aby załadować poprawną AssemblyFile:

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

Następne kroki

Wiele zadań obejmuje wywoływanie pliku wykonywalnego. W niektórych scenariuszach można użyć zadania Exec, ale jeśli ograniczenia zadania Exec są problemem, możesz również utworzyć zadanie niestandardowe. W poniższym samouczku przedstawiono obie opcje z bardziej realistycznym scenariuszem generowania kodu: tworzenie niestandardowego zadania w celu wygenerowania kodu klienta dla interfejsu API REST.

Możesz też dowiedzieć się, jak przetestować zadanie niestandardowe.