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.
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.
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; }
Podczas tworzenia zadania niestandardowego dziedziczysz z Microsoft.Build.Utilities.Task. Aby zaimplementować zadanie, należy zastąpić metodę Execute() . Metoda
Execute
zwracatrue
wartość , jeśli zadanie powiedzie się ifalse
w przeciwnym razie.Task
implementuje Microsoft.Build.Framework.ITask i zapewnia domyślne implementacje niektórychITask
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.
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.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 elemecieCoreClean
docelowym, ustawiającAfterTargets
atrybut naCoreClean
.<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
iOutputs
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.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!
Skompiluj ponownie, a wygenerowany plik powinien zostać utworzony i skompilowany. Sprawdź folder projektu dla pliku MySetting.generated.cs .
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>
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.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.binlog
dziennika, 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 PackageReference
ProjectReference
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ę.
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>
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>
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):
Zmień plik projektu, aby używał poleceń
net472
inet6.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ę ztasks/
natasks/<TFM>/
.<TargetFrameworks>net472;net6.0</TargetFrameworks>
.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 docelowegonet472
projektu inet6.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 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.