Tutoriel : Créez une tâche personnalisée pour la génération de code

Dans ce tutoriel, vous allez créer une tâche personnalisée dans MSBuild en C# qui gère la génération de code, puis vous allez utiliser la tâche dans une build. Cet exemple montre comment utiliser MSBuild pour gérer les opérations de nettoyage et de régénération. L’exemple montre également comment prendre en charge la build incrémentielle, afin que le code soit généré uniquement lorsque les fichiers d’entrée ont changé. Les techniques présentées s’appliquent à un large éventail de scénarios de génération de code. Les étapes montrent également l’utilisation de NuGet pour empaqueter la tâche à des fins de distribution, et le tutoriel inclut une étape facultative pour utiliser la visionneuse BinLog afin d’améliorer l’expérience de résolution des problèmes.

Prérequis

Vous devez avoir une compréhension des concepts MSBuild tels que les tâches, les cibles et les propriétés. Consultez Concepts MSBuild.

Les exemples nécessitent MSBuild, qui est installé avec Visual Studio, mais qui peut également être installé séparément. Consultez Télécharger MSBuild sans Visual Studio.

Introduction à l’exemple de code

L’exemple prend un fichier texte d’entrée contenant des valeurs à définir et crée un fichier de code C# avec du code qui crée ces valeurs. Bien qu’il s’agisse d’un exemple simple, les mêmes techniques de base peuvent être appliquées à des scénarios de génération de code plus complexes.

Dans ce tutoriel, vous allez créer une tâche personnalisée MSBuild nommée AppSettingStronglyTyped. La tâche lira un ensemble de fichiers texte et chaque fichier contiendra des lignes au format suivant :

propertyName:type:defaultValue

Le code génère une classe C# avec toutes les constantes. Un problème doit interrompre la build et donner à l’utilisateur suffisamment d’informations pour diagnostiquer le problème.

L’exemple de code complet de ce tutoriel se trouve dans Tâche personnalisée - Génération de code dans le référentiel d’exemples .NET sur GitHub.

Créez le projet AppSettingStronglyTyped

Créez une bibliothèque de classes .NET Standard. L’infrastructure doit être .NET Standard 2.0.

Notez la différence entre MSBuild complet (celui que Visual Studio utilise) et MSBuild portable, celui fourni dans la ligne de commande .NET Core.

  • MsBuild complet : cette version de MSBuild se trouve généralement dans Visual Studio. S’exécute sur .NET Framework. Visual Studio l’utilise lorsque vous exécutez Build sur votre solution ou projet. Cette version est également disponible à partir d’un environnement de ligne de commande, tel que l’invite de commandes du développeur Visual Studio ou PowerShell.
  • .NET MSBuild : cette version de MSBuild est fournie dans la ligne de commande .NET Core. Il s’exécute sur .NET Core. Visual Studio n’invoque pas directement cette version de MSBuild. Il prend uniquement en charge les projets qui sont générés à l’aide de Microsoft.NET.Sdk.

si vous souhaitez partager du code entre .NET Framework et toute autre implémentation de .NET, telle que .NET Core, votre bibliothèque doit cibler .NET Standard 2.0 et vous souhaitez exécuter dans Visual Studio, qui s’exécute sur le .NET Framework. .NET Framework ne prend pas en charge .NET Standard 2.1.

Créez la tâche personnalisée MSBuild AppSettingStronglyTyped

La première étape consiste à créer la tâche personnalisée MSBuild. des informations sur l’écriture d’une tâche personnalisée MSBuild peuvent vous aider à comprendre les étapes suivantes. Une tâche personnalisée MSBuild est une classe qui implémente l’interface ITask.

  1. Ajoutez une référence au package NuGet Microsoft.Build.Utilities.Core, puis créez une classe nommée AppSettingStronglyTyped dérivée de Microsoft.Build.Utilities.Task.

  2. Ajoutez trois propriétés. Ces propriétés définissent les paramètres de la tâche que les utilisateurs définissent lorsqu’ils utilisent la tâche dans un projet client :

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

    La tâche traite les SettingFiles et génère une classe SettingNamespaceName.SettingClassName. La classe générée aura un ensemble de constantes basées sur le contenu du fichier texte.

    La sortie de la tâche doit être une chaîne qui donne le nom de fichier du code généré :

     // The filename where the class was generated
     [Output]
     public string ClassNameFile { get; set; }
    
  3. Lorsque vous créez une tâche personnalisée, vous héritez de Microsoft.Build.Utilities.Task. Pour implémenter la tâche, vous remplacez la méthode Execute(). La méthode Execute renvoie true si la tâche réussit et false dans le cas contraire. Task implémente Microsoft.Build.Framework.ITask et fournit des implémentations par défaut de certains membres ITask et fournit également certaines fonctionnalités de journalisation. Il est important d’indiquer l’état dans le journal pour diagnostiquer et dépanner la tâche, en particulier si un problème survient et que la tâche doit renvoyer un résultat d’erreur (false). En cas d’erreur, la classe signale l’erreur en appelant 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;
     }
    

    L’API De tâche permet de renvoyer false, indiquant un échec, sans indiquer à l’utilisateur ce qui n’a pas fonctionné. Il est préférable de renvoyer !Log.HasLoggedErrors au lieu d’un code booléen et de journaliser une erreur en cas de problème.

Erreurs de journalisation

La meilleure pratique lors de la journalisation des erreurs consiste à fournir des détails tels que le numéro de ligne et un code d’erreur distinct lors de la journalisation d’une erreur. Le code suivant analyse le fichier d’entrée de texte et utilise la méthode TaskLoggingHelper.LogError avec le numéro de ligne dans le fichier texte qui a généré l’erreur.

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

À l’aide des techniques indiquées dans le code précédent, les erreurs dans la syntaxe du fichier d’entrée de texte s’affichent en tant qu’erreurs de build avec des informations de diagnostic utiles :

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)

Lorsque vous interceptez des exceptions dans votre tâche, utilisez la méthode TaskLoggingHelper.LogErrorFromException. Cela améliorera la sortie d’erreur, par exemple en obtenant la pile des appels où l’exception a été levée.

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

L’implémentation des autres méthodes qui utilisent ces entrées pour générer le texte du fichier de code généré n’est pas affichée ici ; consultez AppSettingStronglyTyped.cs dans l’exemple de référentiel.

L’exemple de code génère du code C# pendant le processus de build. La tâche est comme n’importe quelle autre classe C#. Par conséquent, lorsque vous avez terminé ce tutoriel, vous pouvez la personnaliser et ajouter les fonctionnalités nécessaires à votre propre scénario.

Générez une application console et utilisez la tâche personnalisée

Dans cette section, vous allez créer une application console .NET Core standard qui utilise la tâche.

Important

Il est important d’éviter de générer une tâche personnalisée MSBuild dans le même processus MSBuild qui va l’utiliser. Le nouveau projet doit se trouver dans une solution Visual Studio complètement différente, ou le nouveau projet doit utiliser une dll pré-générée et relocalisée à partir de la sortie standard.

  1. Créez le projet console .NET MSBuildConsoleExample dans une nouvelle solution Visual Studio.

    La façon normale de distribuer une tâche consiste à utiliser un package NuGet, mais pendant le développement et le débogage, vous pouvez inclure toutes les informations sur .props et .targets directement dans le fichier projet de votre application, puis passer au format NuGet lorsque vous distribuez la tâche à d’autres personnes.

  2. Modifiez le fichier projet pour utiliser la tâche de génération de code. La liste de code de cette section montre le fichier projet modifié après avoir référencé la tâche, défini les paramètres d’entrée de la tâche et écrit les cibles de gestion des opérations de nettoyage et de rebuild afin que le fichier de code généré soit supprimé comme prévu.

    Les tâches sont inscrites à l’aide de l’élément UsingTask (MSBuild). L’élément UsingTask inscrit la tâche ; il indique à MSBuild le nom de la tâche et comment localiser et exécuter l’assembly qui contient la classe de tâche. Le chemin d’accès de l’assembly est relatif au fichier projet.

    Le PropertyGroup contient les définitions de propriétés qui correspondent aux propriétés définies dans la tâche. Ces propriétés sont définies à l’aide d’attributs et le nom de la tâche est utilisé comme nom d’élément.

    TaskName est le nom de la tâche à référencer à partir de l’assembly. Cet attribut doit toujours utiliser des espaces de noms entièrement spécifiés. AssemblyFile est le chemin d’accès au fichier de l’assembly.

    Pour invoquer la tâche, ajoutez la tâche à la cible appropriée, dans ce cas GenerateSetting.

    La cible ForceGenerateOnRebuild gère les opérations de nettoyage et de rebuild en supprimant le fichier généré. Elle est définie pour s’exécuter après la cible CoreClean en définissant l’attribut AfterTargets sur 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>
    

    Notes

    Au lieu de remplacer une cible telle que CoreClean, ce code utilise une autre façon d’ordonner les cibles (BeforeTarget et AfterTarget). Les projets de style SDK ont une importation implicite de cibles après la dernière ligne du fichier projet ; cela signifie que vous ne pouvez pas remplacer les cibles par défaut, sauf si vous spécifiez vos importations manuellement. Consultez Remplacer les cibles prédéfinies.

    Les attributs Inputs et Outputs aident MSBuild à être plus efficaces en fournissant des informations pour les builds incrémentielles. Les dates des entrées sont comparées aux sorties pour voir si la cible doit être exécutée ou si la sortie de la build précédente peut être réutilisée.

  3. Créez le fichier texte d’entrée avec l’extension à découvrir. À l’aide de l’extension par défaut, créez MyValues.mysettings à la racine, avec le contenu suivant :

     Greeting:string:Hello World!
    
  4. Générez à nouveau et le fichier généré doit être créé et généré. Vérifiez le fichier MySetting.generated.cs dans le dossier du projet.

  5. La classe MySetting se trouve dans l’espace de noms incorrect, alors modifiez-la pour utiliser l’espace de noms de notre application. Ouvrez le fichier projet et ajoutez le code suivant :

     <PropertyGroup>
     	<SettingNamespace>MSBuildConsoleExample</SettingNamespace>
     </PropertyGroup>
    
  6. Régénérez à nouveau et observez que la classe se trouve dans l’espace de noms MSBuildConsoleExample. De cette façon, vous pouvez redéfinir le nom de classe généré (SettingClass), les fichiers d’extension de texte (SettingExtensionFile) à utiliser comme entrée et leur emplacement (RootFolder) si vous le souhaitez.

  7. Ouvrez Program.cs et modifiez le « Hello World!! » codé en dur à la constante définie par l’utilisateur :

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

Exécutez le programme ; il imprimera le message d’accueil de la classe générée.

(Facultatif) Événements de journalisation pendant le processus de build

Il est possible de compiler à l’aide d’une commande de ligne de commande. Accédez au dossier du projet. Vous utiliserez l’option -bl (journal binaire) pour générer un journal binaire. Le journal binaire aura des informations utiles pour savoir ce qui se passe pendant le processus de build.

# 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

Les deux commandes génèrent un fichier journal msbuild.binlog, qui peut être ouvert avec MSBuild Binary et Structured Log Viewer. L’option /t:rebuild signifie qu’il faut exécuter la cible de rebuild. Cela forcera la régénération du fichier de code généré.

Félicitations ! Vous avez créé une tâche qui génère du code et vous l’avez utilisée dans une build.

Empaquetez la tâche pour la distribution

Si vous n’avez besoin d’utiliser votre tâche personnalisée que dans quelques projets ou dans une seule solution, la consommation de la tâche en tant qu’assembly brut peut être tout ce dont vous avez besoin, mais la meilleure façon de préparer votre tâche pour l’utiliser ailleurs ou la partager avec d’autres personnes est un package NuGet.

Les packages de tâches MSBuild présentent quelques différences clés par rapport aux packages NuGet de bibliothèque :

  • Ils doivent regrouper leurs propres dépendances d’assembly, au lieu d’exposer ces dépendances au projet consommateur
  • Ils n’empaquettent pas les assemblys requis dans un dossier lib/<target framework>, car NuGet inclurait les assemblys dans tout package qui consomme la tâche
  • Ils ne doivent compiler que les assemblys Microsoft.Build - au moment de l’exécution, ceux-ci seront fournis par le moteur MSBuild et n’ont donc pas besoin d’être inclus dans le paquet
  • Ils génèrent un fichier spécial .deps.json qui aide MSBuild à charger les dépendances de la tâche (en particulier les dépendances natives) de manière cohérente

Pour atteindre tous ces objectifs, vous devez apporter quelques modifications au fichier projet standard au-delà de celles que vous connaissez peut-être.

Créer un package NuGet

La création d’un package NuGet est la méthode recommandée pour distribuer votre tâche personnalisée à d’autres personnes.

Préparez la génération du package

Pour préparer la génération d’un package NuGet, apportez des modifications au fichier projet afin de spécifier les détails qui décrivent le package. Le fichier projet initial que vous avez créé ressemble au code suivant :

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

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

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

</Project>

Pour générer un package NuGet, ajoutez le code suivant pour définir les propriétés du package. Vous pouvez voir la liste complète des propriétés MSBuild prises en charge dans la documentation 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>
	...
</PropertyGroup>

Marquez les dépendances comme privées

Les dépendances de votre tâche MSBuild doivent être empaquetées à l’intérieur du package ; elles ne peuvent pas être exprimées sous forme de références de package normales. Le package n’expose pas de dépendances régulières aux utilisateurs externes. Pour ce faire, deux étapes sont nécessaires : marquez vos assemblys comme privés et les incorporer dans le package généré. Pour cet exemple, nous partons du principe que votre tâche dépend de Microsoft.Extensions.DependencyInjection pour fonctionner. Ajoutez donc un PackageReference à Microsoft.Extensions.DependencyInjection à la version 6.0.0.

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

À présent, marquez chaque dépendance de ce projet de tâche, à la fois PackageReference et ProjectReference avec l’attribut PrivateAssets="all". Cela indiquera à NuGet de ne pas exposer ces dépendances aux projets qui consomment. Pour en savoir plus sur le contrôle des ressources de dépendance, consultez la documentation 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>

Regroupez les dépendances dans le package

Vous devez également incorporer les ressources d’exécution de nos dépendances dans le package de tâches. Il existe deux parties : une cible MSBuild qui ajoute nos dépendances à ItemGroup BuildOutputInPackage et quelques propriétés qui contrôlent la disposition de ces éléments BuildOutputInPackage. Vous pouvez en savoir plus sur ce processus dans la documentation 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>

Ne regroupez pas l’assembly Microsoft.Build.Utilities.Core

Comme indiqué ci-dessus, cette dépendance sera fournie par MSBuild au moment de l’exécution. Nous n’avons donc pas besoin de la regrouper dans le package. Pour ce faire, ajoutez l’attribut ExcludeAssets="Runtime" à l’attribut PackageReference pour cette dépendance

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

Générez et incorporez un fichier deps.json

Le fichier deps.json peut être utilisé par MSBuild pour s’assurer que les versions correctes de vos dépendances sont chargées. Vous devrez ajouter des propriétés MSBuild pour générer le fichier, car il n’est pas généré par défaut pour les bibliothèques. Ensuite, ajoutez une cible pour l’inclure dans notre sortie de package, de la même façon que vous l’avez fait pour nos dépendances de package.

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

Incluez des cibles et des propriétés MSBuild dans un package

Pour plus d’informations sur cette section, découvrez les propriétés et les cibles , puis comment inclure des propriétés et des cibles dans un package NuGet.

Vous pouvez être amené à ajouter des cibles ou propriétés de build personnalisées dans les projets qui utilisent votre package, comme dans le cas de l’exécution d’un processus ou outil personnalisé pendant la génération. Pour ce faire, placez des fichiers sous la forme <package_id>.targets ou <package_id>.props dans le dossier build du projet.

Les fichiers du dossier build racine du projet sont considérés comme adaptés à toutes les infrastructures cibles.

Dans cette section, vous allez connecter l’implémentation de la tâche dans les fichiers .props et .targets, qui seront inclus dans notre package NuGet et chargés automatiquement à partir d’un projet de référencement.

  1. Dans le fichier projet de la tâche, AppSettingStronglyTyped.csproj, ajoutez le code suivant :

     <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. Créez un dossier build et, dans ce dossier, ajoutez deux fichiers texte : AppSettingStronglyTyped.props et AppSettingStronglyTyped.targets. AppSettingStronglyTyped.props est importé tôt dans Microsoft.Common.props et les propriétés définies ultérieurement ne sont pas disponibles pour ce dernier. Évitez donc de faire référence à des propriétés qui ne sont pas encore définies ; elles seraient évaluées comme étant vides.

    Directory.Build.targets est importé à partir de Microsoft.Common.targets après l’importation des fichiers .targets à partir des packages NuGet. Par conséquent, il peut remplacer les propriétés et les cibles définies dans la plupart de la logique de génération, ou définir des propriétés pour tous vos projets, indépendamment de ce que les projets individuels définissent. Consultez l’ordre d’importation.

    AppSettingStronglyTyped.props inclut la tâche et définit certaines propriétés avec des valeurs par défaut :

     <?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. Le fichier AppSettingStronglyTyped.props est automatiquement inclus lorsque le package est installé. Ensuite, le client dispose de la tâche disponible et de certaines valeurs par défaut. Toutefois, il n’est jamais utilisé. Pour mettre ce code en action, définissez certaines cibles dans le fichier AppSettingStronglyTyped.targets, qui seront également automatiquement incluses lors de l’installation du package :

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

    La première étape est la création d’un ItemGroup, qui représente les fichiers texte (il peut y en avoir plusieurs) à lire et qui constituera une partie des paramètres de notre tâche. Il existe des valeurs par défaut pour l’emplacement et l’extension que nous recherchons, mais vous pouvez remplacer les valeurs qui définissent les propriétés dans le fichier projet MSBuild client.

    Définissez ensuite deux cibles MSBuild. Nous étendons le processus MSBuild, en remplaçant les cibles prédéfinies :

    • BeforeCompile : l’objectif est d’appeler la tâche personnalisée pour générer la classe et d’inclure la classe à compiler. Les tâches de cette cible sont insérées avant la compilation principale. Les champs d’entrée et de sortie sont liés à la build incrémentielle. Si tous les éléments de sortie sont à jour, MSBuild ignore la cible. Cette build incrémentielle de la cible peut améliorer considérablement les performances de vos builds. Un élément est considéré comme à jour si son fichier de sortie a une date de création identique ou antérieure à celle du ou des fichiers d’entrée.

    • AfterClean : l’objectif est de supprimer le fichier de classe généré après un nettoyage général. Les tâches de cette cible sont insérées après l’appel de la fonctionnalité de nettoyage principale. Elle force la répétition de l’étape de génération de code lorsque la cible de Rebuild s’exécute.

Générez le package NuGet

Pour générer le package NuGet, vous pouvez utiliser Visual Studio (faites un clic droit sur le nœud du projet dans Explorateur de solutions, puis sélectionnez Pack). Vous pouvez également le faire à l’aide de la ligne de commande. Accédez au dossier où le fichier projet de tâche AppSettingStronglyTyped.csproj est présent, puis exécutez la commande suivante :

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

Félicitations ! Vous avez généré un package NuGet nommé \AppSettingStronglyTyped\AppSettingStronglyTyped\AppSettingStronglyTyped.1.0.0.nupkg.

Le package a une extension .nupkg et est un fichier zip compressé. Vous pouvez l’ouvrir avec un outil zip. Les fichiers .target et .props se trouvent dans le dossier build. Le fichier .dll se trouve dans le dossier lib\netstandard2.0\. Le fichier AppSettingStronglyTyped.nuspec se trouve au niveau racine.

(Facultatif) Prise en charge du multi-ciblage

Vous devriez envisager de prendre en charge les distributions Full MSBuild (.NET Framework) et Core (y compris .NET 5 et versions ultérieures) de MSBuild afin de prendre en charge la base d’utilisateurs la plus large possible.

Pour les projets de SDK .NET « normaux », le multi-ciblage signifie que vous devez définir plusieurs TargetFrameworks dans votre fichier de projet. Lorsque vous effectuez cette opération, les builds seront déclenchées pour les TargetFrameworkMonikers, et les résultats globaux peuvent être empaquetés en tant qu’artefact unique.

Ce n’est pas l’histoire complète de MSBuild. MSBuild a deux principaux véhicules d’expédition : Visual Studio et le SDK .NET. Il s’agit d’environnements d’exécution très différents ; l’un fonctionne avec .NET Framework et l’autre avec CoreCLR. Cela signifie que si votre code peut cibler netstandard2.0, votre logique de tâche peut présenter des différences en fonction du type d’exécution MSBuild actuellement utilisé. En pratique, étant donné qu’il y a tant de nouvelles API dans .NET 5.0 et dans les versions ultérieures, il est judicieux de multi-cibler à la fois votre code source de tâche MSBuild pour plusieurs TargetFrameworkMonikers, ainsi que de multi-cibler votre logique cible MSBuild pour plusieurs types d’exécution MSBuild.

Modifications requises pour multi-cibler

Pour cibler plusieurs TargetFrameworkMonikers (TFM) :

  1. Modifiez votre fichier projet pour utiliser les net472 et net6.0 TFMs (ces derniers peuvent changer en fonction du niveau du SDK que vous souhaitez cibler). Vous souhaiterez peut-être cibler netcoreapp3.1 jusqu’à ce que .NET Core 3.1 ne soit plus pris en charge. Lorsque vous effectuez cette opération, la structure du dossier de package passe de tasks/ à tasks/<TFM>/.

    <TargetFrameworks>net472;net6.0</TargetFrameworks>
    
  2. Mettez à jour vos fichiers .targets pour utiliser le TFM approprié pour charger vos tâches. Le TFM requis changera en fonction du TFM .NET que vous avez choisi ci-dessus, mais pour un projet ciblant net472 et net6.0, vous auriez une propriété comme :

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

Ce code utilise la propriété MSBuildRuntimeType comme proxy pour l’environnement d’hébergement actif. Une fois cette propriété définie, vous pouvez l’utiliser dans UsingTask pour charger la valeur AssemblyFile correcte :

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

Étapes suivantes

De nombreuses tâches impliquent l’appel d’un exécutable. Dans certains scénarios, vous pouvez utiliser la tâche Exec, mais si les limitations de la tâche Exec sont un problème, vous pouvez également créer une tâche personnalisée. Le tutoriel suivant décrit les deux options avec un scénario de génération de code plus réaliste : création d’une tâche personnalisée pour générer du code client pour une API REST.

Vous pouvez également découvrir comment tester une tâche personnalisée.