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.

Wymagania 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 Pobieranie programu 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 odczytuje zestaw plików tekstowych, a każdy plik z wierszami 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 temacie Niestandardowe zadanie — generowanie kodu w repozytorium przykładów platformy .NET w witrynie GitHub.

Tworzenie projektu AppSettingStronglyTyped

Utwórz bibliotekę klas .NET Standard. Struktura powinna mieć wartość .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 tej funkcji podczas wykonywania polecenia Build on your solution or project (Kompilowanie 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 dla platformy .NET Standard 2.0 i chcesz uruchomić ją w programie Visual Studio, który działa w programie .NET Framework. Program .NET Framework nie obsługuje platformy .NET Standard 2.1.

Tworzenie niestandardowego zadania appSettingStronglyTyped MSBuild

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

  1. Dodaj odwołanie do pakietu NuGet Microsoft.Build.Utilities.Core , a następnie utwórz klasę o nazwie AppSettingStronglyTyped pochodzącą z pliku 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 plik 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 wartość , jeśli zadanie powiedzie się i false w przeciwnym razie. Task implementuje Microsoft.Build.Framework.ITask i zapewnia domyślne implementacje niektórych ITask elementów członkowskich, a ponadto zapewnia pewne funkcje rejestrowania. Ważne jest, aby wyświetlić stan danych wyjściowych dziennika, aby zdiagnozować i rozwiązać problem, zwłaszcza jeśli wystąpi problem, a zadanie musi zwrócić wynik błędu (false). Po błędzie klasa sygnalizuje błąd, wywołując metodę 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 dziennika

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 TaskLoggingHelper.LogError metody 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 TaskLoggingHelper.LogErrorFromException metody . 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.

Generowanie aplikacji konsolowej i używanie zadania niestandardowego

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

Ważne

Ważne jest, aby uniknąć generowania niestandardowego zadania MSBuild w tym samym procesie MSBuild, który będzie go używać. Nowy projekt powinien znajdować się w całkowicie innym rozwiązaniu programu Visual Studio lub nowy projekt używa wstępnie wygenerowanej biblioteki DLL i ponownej lokalizacji na podstawie standardowych danych wyjściowych.

  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 dołączyć wszystkie informacje i .props.targets bezpośrednio do 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.

    Zawiera PropertyGroup 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 to nazwa zadania do odwołania z zestawu. Ten atrybut powinien zawsze używać w pełni określonych przestrzeni nazw. AssemblyFile to ścieżka pliku zestawu.

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

    Obiekt docelowy ForceGenerateOnRebuild obsługuje operacje czyszczenia i ponownego kompilowania przez usunięcie wygenerowanego pliku. Jest ona ustawiona tak, aby była uruchamiana po elemecie CoreClean docelowym, ustawiając AfterTargets atrybut na 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>
    

    Uwaga

    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 obiektów docelowych.

    Atrybuty Inputs i Outputs ułatwiają programowi MSBuild wydajniejsze dostarczanie informacji dotyczących 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. Za pomocą rozszerzenia domyślnego utwórz MyValues.mysettings element 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. Ponownie ponownie skompiluj MSBuildConsoleExample i zwróć uwagę, że klasa znajduje się w przestrzeni nazw. W ten sposób można ponownie zdefiniować wygenerowaną nazwę klasy (SettingClass), pliki rozszerzenia tekstu (SettingExtensionFile), które mają być używane jako dane wejściowe, oraz 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 wiersza polecenia. Przejdź do folderu projektu. Użyjesz -bl opcji (dziennika binarnego), 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 msbuild.binlogdziennika, który można otworzyć za pomocą pliku binarnego MSBuild i przeglądarki dzienników strukturalnych. Opcja /t:rebuild oznacza uruchomienie obiektu docelowego ponownej kompilacji. 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 używać zadania niestandardowego tylko w kilku projektach lub w jednym rozwiązaniu, użycie zadania jako zestawu pierwotnego może być potrzebne, ale najlepszym sposobem przygotowania zadania do użycia go w innym miejscu lub udostępnienia go innym osobom jest pakiet NuGet.

Pakiety zadań MSBuild mają kilka kluczowych różnic między pakietami NuGet biblioteki:

  • Muszą one powiązać własne zależności zestawów zamiast uwidaczniać te zależności w projekcie zużywającym
  • Nie pakują żadnych wymaganych zestawów do lib/<target framework> folderu, ponieważ spowodowałoby to dołączenie pakietów NuGet do dowolnego pakietu, który zużywa zadanie
  • Muszą tylko skompilować zestawy Microsoft.Build — w czasie wykonywania będą one udostępniane przez rzeczywisty aparat MSBuild i dlatego nie muszą być uwzględnione w pakiecie
  • Generują one specjalny .deps.json plik, 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 powyżej i poza tymi, które możesz 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:

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

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: oznaczanie zestawów jako prywatnych i faktycznie osadzanie ich w wygenerowanych pakietach. W tym przykładzie przyjęto założenie, że zadanie zależy od Microsoft.Extensions.DependencyInjection pracy, więc dodaj element PackageReference do Microsoft.Extensions.DependencyInjection elementu 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 i PackageReferenceProjectReference za pomocą atrybutu PrivateAssets="all" . Spowoduje to, że program NuGet nie uwidacznia tych zależności do korzystania z projektów w ogóle. 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>

Łączenie zależności z pakietem

Należy również osadzić zasoby środowiska uruchomieniowego naszych zależności w pakiecie Zadań. Istnieją dwie części: docelowy program MSBuild, który dodaje nasze zależności do BuildOutputInPackage elementu ItemGroup i kilka właściwości kontrolujących układ tych BuildOutputInPackage elementów. Więcej informacji na temat tego procesu można uzyskać w dokumentacji narzędzia NuGet.

<PropertyGroup>
	...
	<!-- This target will run when MSBuild is collecting the files to be packaged, and we'll implement it below. This property controls the dependency list for this packaging process, so by adding our custom property we hook ourselves into the process in a supported way. -->
	<TargetsForTfmSpecificBuildOutput>
		$(TargetsForTfmSpecificBuildOutput);CopyProjectReferencesToPackage
	</TargetsForTfmSpecificBuildOutput>
	<!-- This property tells MSBuild where the root folder of the package's build assets should be. Because we are not a library package, we should not pack to 'lib'. Instead, we choose 'tasks' by convention. -->
	<BuildOutputTargetFolder>tasks</BuildOutputTargetFolder>
	<!-- NuGet does validation that libraries in a package are exposed as dependencies, but we _explicitly_ do not want that behavior for MSBuild tasks. They are isolated by design. Therefore we ignore this specific warning. -->
	<NoWarn>NU5100</NoWarn>
	...
</PropertyGroup>

...
<!-- This is the target we defined above. It's purpose is to add all of our PackageReference and ProjectReference's runtime assets to our package output.  -->
<Target
	Name="CopyProjectReferencesToPackage"
	DependsOnTargets="ResolveReferences">
	<ItemGroup>
		<!-- The TargetPath is the path inside the package that the source file will be placed. This is already precomputed in the ReferenceCopyLocalPaths items' DestinationSubPath, so reuse it here. -->
		<BuildOutputInPackage
			Include="@(ReferenceCopyLocalPaths)"
			TargetPath="%(ReferenceCopyLocalPaths.DestinationSubPath)" />
	</ItemGroup>
</Target>

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

Jak wspomniano powyżej, ta zależność zostanie udostępniona przez program MSBuild w czasie wykonywania, więc nie musimy ich pakować do pakietu. W tym celu dodaj ExcludeAssets="Runtime" do niego atrybut 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

W tle w tej sekcji przeczytaj o właściwościach i elementach 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 build folderze w projekcie.

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

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

  1. W pliku projektu zadania AppSettingStronglyTyped.csproj dodaj 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. Element AppSettingStronglyTyped.props jest importowany wcześnie w pliku Microsoft.Common.props, a zdefiniowane później właściwości są niedostępne. Dlatego unikaj odwoływania się do właściwości, które nie są jeszcze zdefiniowane; będą oceniać, że są puste.

    Pliki Directory.Build.targets są importowane z obiektów Microsoft.Common.targets po zaimportowaniu .targets plików z pakietów NuGet. Dlatego może zastąpić właściwości i obiekty docelowe zdefiniowane w większości logiki kompilacji lub ustawić właściwości dla wszystkich projektów niezależnie od tego, co ustawić poszczególne projekty. Zobacz Import Order (Kolejność importowania).

    Element 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 akcji, 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 grupy elementów, która 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 programu MSBuild. Rozszerzamy proces MSBuild, przesłaniając wstępnie zdefiniowane cele:

    • BeforeCompile: Celem jest wywołanie zadania niestandardowego w celu wygenerowania klasy i dołączenie klasy do skompilowania. Zadania w tym obiekcie docelowym są wstawiane przed wykonaniem kompilacji podstawowej. Pola wejściowe i wyjściowe są powiązane z kompilacją przyrostową. Jeśli wszystkie elementy wyjściowe są aktualne, program MSBuild pomija element docelowy. Ta przyrostowa kompilacja celu może znacznie poprawić wydajność kompilacji. Element uważa się za aktualny, jeśli plik wyjściowy jest nie starszy niż plik lub pliki wejściowe.

    • 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 to powtórzenie kroku generowania kodu po wykonaniu obiektu docelowego Ponowne kompilowanie.

Generowanie pakietu NuGet

Aby wygenerować pakiet NuGet, możesz użyć programu Visual Studio (kliknij prawym przyciskiem myszy węzeł projektu w Eksplorator rozwiązań, a następnie wybierz pozycję Pack (Pakiet). Można to również zrobić przy użyciu wiersza polecenia. Przejdź do folderu, w którym znajduje się plik projektu zadania 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ę zarówno Full (.NET Framework), jak i Core (w tym dystrybucji MSBuild platformy .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. W takim przypadku kompilacje zostaną wyzwolone dla elementów TargetFrameworkMonikers, a ogólne wyniki można spakować jako pojedynczy artefakt.

To nie jest pełna historia programu MSBuild. Program MSBuild ma dwa podstawowe pojazdy wysyłkowe: Program Visual Studio i zestaw .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, ponieważ istnieje tak wiele nowych interfejsów API na platformie .NET 5.0 i nowszych, warto uruchomić kod źródłowy zadania MSBuild dla wielu obiektów TargetFrameworkMonikers, a także multitarget logiki docelowej MSBuild dla wielu typów środowiska uruchomieniowego MSBuild.

Zmiany wymagane do wielotargetu

Aby określić wiele elementów TargetFrameworkMonikers (TFM):

  1. Zmień plik projektu, aby używał poleceń net472 i net6.0 TFM (ten ostatni może ulec zmianie w zależności od poziomu zestawu SDK, który ma być docelowy). Możesz chcieć kierować netcoreapp3.1 do momentu, aż platforma .NET Core 3.1 nie będzie obsługiwać. Gdy to zrobisz, struktura folderu pakietu zmieni się z tasks/ na tasks/<TFM>/.

    <TargetFrameworks>net472;net6.0</TargetFrameworks>
    
  2. .targets Zaktualizuj pliki, aby załadować zadania przy użyciu poprawnego serwera TFM. Wymagany program TFM zmieni się w zależności od wybranego powyżej programu .NET TFM, ale w przypadku elementu docelowego net472 projektu i net6.0wł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 MSBuildRuntimeType właściwości jako serwera proxy dla aktywnego środowiska hostingu. Po ustawieniu tej właściwości można jej użyć w elemecie UsingTask , aby załadować poprawną właściwość 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.