Partage via


Tutoriel : Créer 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 reconstruction. L’exemple montre également comment prendre en charge la génération 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 de distribution, et le tutoriel inclut une étape facultative pour utiliser la visionneuse BinLog pour améliorer l’expérience de résolution des problèmes.

Conditions préalables

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 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’agit 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 lit un ensemble de fichiers texte, et chaque fichier avec des lignes au format suivant :

propertyName:type:defaultValue

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

L’exemple de code complet de ce didacticiel se trouve à tâche personnalisée : génération de code dans le référentiel d’exemples .NET sur GitHub.

Créer le projet AppSettingStronglyTyped

Créez une bibliothèque de classes .NET Standard. Le framework 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 réside 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 groupée dans la ligne de commande .NET Core. Il s’exécute sur .NET Core. Visual Studio n’appelle pas directement cette version de MSBuild. Il prend uniquement en charge les projets qui utilisent Microsoft.NET.Sdk.

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

Choisir la version de l’API MSBuild à référencer

Lors de la compilation d’une tâche personnalisée, vous devez référencer la version de l’API MSBuild (Microsoft.Build.*) qui correspond à la version minimale de Visual Studio et/ou au SDK .NET que vous prévoyez de prendre en charge. Par exemple, pour prendre en charge les utilisateurs de Visual Studio 2019, vous devez compiler avec MSBuild 16.11.

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 la façon de écrire 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 le SettingFiles et génère une classe SettingNamespaceName.SettingClassName. La classe générée aura un ensemble de constantes en fonction du 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 retourne true si la tâche réussit et false sinon. 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 de générer l’état du journal pour diagnostiquer et résoudre la tâche, en particulier si un problème se produit et que la tâche doit retourner 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 retourner false, indiquant l’échec, sans indiquer à l’utilisateur ce qui s’est passé. Il est préférable de retourner !Log.HasLoggedErrors au lieu d’un code booléen et de consigner une erreur lorsqu’un problème se produit.

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 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 présenté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 génération 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 dépôt.

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

Générer une application console et utiliser 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 autre solution Visual Studio, ou le nouveau projet utilise une dll prégénéisé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 affiche le fichier projet modifié après avoir référencé la tâche, en définissant les paramètres d’entrée de la tâche et en écrivant les cibles pour gérer les opérations de nettoyage et de reconstruction 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 à 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 appeler 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 reconstruction 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>
    

    Remarque

    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 sur 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 dossier du projet pour le fichier MySetting.generated.cs.

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

    <PropertyGroup>
        <SettingNamespace>MSBuildConsoleExample</SettingNamespace>
    </PropertyGroup>
    
  6. Regénérer à nouveau et observer 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 l’emplacement (RootFolder) d’entre eux 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 imprime le message d’accueil de la classe générée.

(Facultatif) Journaliser les événements pendant le processus de génération

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 contiendra des informations utiles pour savoir ce qui se passe pendant le processus de compilation.

# 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 exécuter l'objectif de reconstruction. Elle force 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 l’a utilisée dans une build.

Empaqueter 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 solution unique, l’utilisation 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 utilisateurs est en tant que package NuGet.

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

  • Ils doivent regrouper leurs propres dépendances, au lieu d’exposer ces dépendances au projet utilisateur.
  • Ils n'emballent pas les assemblages requis dans un dossier lib/<target framework>, car cela entraînerait NuGet à inclure les assemblages dans tout package qui utilise 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 .deps.json spécial 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 ci-dessus et au-delà de ceux 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éparer la génération du package

Pour préparer la génération d’un package NuGet, apportez des modifications au fichier projet pour 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>
    <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
    ...
</PropertyGroup>

La propriété CopyLocalLockFileAssemblies est nécessaire pour vous assurer que les dépendances sont copiées dans le répertoire de sortie.

Marquer les dépendances comme privées

Les dépendances de votre tâche MSBuild doivent être empaquetées à l’intérieur du package ; ils ne peuvent pas être exprimés en tant que références de package normales. Le package n’expose pas de dépendances régulières aux utilisateurs externes. Cela nécessite deux étapes pour l'accomplir : marquer vos assemblies comme privés et les intégrer dans le package généré. Pour cet exemple, nous supposons que votre tâche dépend de Microsoft.Extensions.DependencyInjection pour fonctionner, alors ajoutez 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. Vous pouvez en savoir plus sur le contrôle des ressources de dépendance dans 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>

Regrouper les dépendances dans le package

Vous devez également incorporer les ressources runtime de nos dépendances dans le package de tâches. Il existe deux parties à ceci : une cible MSBuild qui ajoute nos dépendances au BuildOutputInPackage ItemGroup, 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>
    <!-- Suppress NuGet warning NU5128. -->
    <SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
    ...
</PropertyGroup>

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

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 les 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érer et incorporer un fichier deps.json

Le fichier deps.json peut être utilisé par MSBuild pour vous assurer que les versions correctes de vos dépendances sont chargées. Vous devez ajouter certaines 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>

Inclure des propriétés et des cibles 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.

Dans certains cas, vous pouvez ajouter des cibles ou des propriétés de build personnalisées dans des projets qui consomment votre package, comme l’exécution d’un outil personnalisé ou d’un processus 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 de compilation à la racine du projet sont considérés comme adaptés à tous les frameworks cibles.

Dans cette section, vous allez intégrer l’implémentation de la tâche dans les fichiers .props et .targets, qui seront inclus dans notre paquet NuGet et chargés automatiquement depuis un projet qui le référence.

  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 build dossier 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. Évitez donc de faire référence aux propriétés qui ne sont pas encore définies ; ils seraient vides.

    Directory.Build.targets est importé à partir de Microsoft.Common.targets après l’importation de fichiers .targets à partir de packages NuGet. Par conséquent, il peut remplacer les propriétés et les cibles définies dans la plupart de la logique de compilation, ou définir des propriétés pour tous vos projets, indépendamment des propriétés définies par les projets individuels. Consultez la commande 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 lors de l’installation du package. Ensuite, le client dispose de la tâche disponible et de certaines valeurs par défaut. Toutefois, il n’est jamais utilisé. Pour placer ce code en action, définissez certaines cibles dans le fichier AppSettingStronglyTyped.targets, qui seront également automatiquement incluses lorsque le package est installé :

    <?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 s’agir de plusieurs) à lire et il s’agit de certains de nos paramètres de tâche. Il existe des valeurs par défaut pour l’emplacement et l’extension où 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 up-to-date, 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 up-to-date si son fichier de sortie est le même âge ou plus récent que son fichier d’entrée ou ses fichiers.

    • 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 reconstruction s’exécute.

Générer le package NuGet

Pour générer le package NuGet, vous pouvez utiliser Visual Studio (cliquez avec le bouton droit sur le nœud du projet dans l’Explorateur de solutions, puis sélectionnez Pack). Vous pouvez également le faire à l’aide de la ligne de commande. Accédez au dossier où se trouve le fichier projet de tâche AppSettingStronglyTyped.csproj, 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 devez envisager de prendre en charge les distributions MSBuild Full (.NET Framework) et Core (y compris .NET 5 et versions ultérieures) pour prendre en charge la base d’utilisateurs la plus large possible.

Pour les projets .NET SDK « normaux », le ciblage multiple signifie 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 véhicules d’expédition principaux : Visual Studio et le Kit de développement logiciel (SDK) .NET. Il s’agit d’environnements d’exécution très différents ; l’une s’exécute sur le runtime .NET Framework, et d’autres s’exécutent sur CoreCLR. Cela signifie que, bien que votre code puisse cibler netstandard2.0, votre logique de tâche peut avoir des différences en fonction de ce que le type d’exécution MSBuild est actuellement en cours d’utilisation. En pratique, étant donné qu’il existe tant de nouvelles API dans .NET 5.0 et versions ultérieures, il est judicieux de multicibler à la fois votre code source de tâche MSBuild pour plusieurs TargetFrameworkMonikers, ainsi que de multicibler votre logique cible MSBuild pour plusieurs types d'environnements d'exécution MSBuild.

Modifications requises pour multitarget

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 pouvez cibler netcoreapp3.1 jusqu’à ce que .NET Core 3.1 ne soit plus pris en charge. Lorsque vous procédez ainsi, la structure de dossiers du 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 en tant que proxy pour l’environnement d’hébergement actif. Une fois cette propriété définie, vous pouvez l’utiliser dans l'UsingTask pour charger la AssemblyFilecorrecte :

<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 apprendre à tester une tâche personnalisée.