Condividi tramite


Esercitazione: Creare un'attività personalizzata per la generazione di codice

In questa esercitazione si creerà un'attività personalizzata in MSBuild in C# che gestisce la generazione del codice e quindi si userà l'attività in una compilazione. Questo esempio illustra come usare MSBuild per gestire le operazioni di pulizia e ricompilazione. L'esempio mostra anche come supportare la compilazione incrementale, in modo che il codice venga generato solo quando i file di input sono stati modificati. Le tecniche illustrate sono applicabili a un'ampia gamma di scenari di generazione di codice. I passaggi illustrano anche l'uso di NuGet per creare un pacchetto dell'attività per la distribuzione e l'esercitazione include un passaggio facoltativo per usare il visualizzatore BinLog per migliorare l'esperienza di risoluzione dei problemi.

Prerequisiti

È necessario avere una conoscenza dei concetti di MSBuild, ad esempio attività, destinazioni e proprietà. Vedere concetti relativi a MSBuild.

Gli esempi richiedono MSBuild, installato con Visual Studio, ma possono anche essere installati separatamente. Vedere Scaricare MSBuild senza Visual Studio.

Introduzione all'esempio di codice

L'esempio accetta un file di testo di input contenente valori da impostare e crea un file di codice C# con codice che crea questi valori. Anche se si tratta di un semplice esempio, le stesse tecniche di base possono essere applicate a scenari di generazione di codice più complessi.

In questa esercitazione, creerai un'attività MSBuild personalizzata denominata AppSettingStronglyTyped. L'attività leggerà un set di file di testo e ciascun file conterrà righe nel formato seguente.

propertyName:type:defaultValue

Il codice genera una classe C# con tutte le costanti. Un problema deve arrestare la compilazione e fornire all'utente informazioni sufficienti per diagnosticare il problema.

Il codice di esempio completo per questa esercitazione è disponibile in Attività personalizzata - generazione di codice nel repository di esempi .NET su GitHub.

Creare il progetto AppSettingStronglyTyped

Creare una libreria di classi .NET Standard. Il framework deve essere .NET Standard 2.0.

Si noti la differenza tra MSBuild completo (quello usato da Visual Studio) e MSBuild portabile, quello in bundle nella riga di comando di .NET Core.

  • MSBuild completo: questa versione di MSBuild si trova in genere all'interno di Visual Studio. Viene eseguito in .NET Framework. Visual Studio utilizza questo quando si esegue Build sulla soluzione o sul progetto. Questa versione è disponibile anche da un ambiente della riga di comando, ad esempio il prompt dei comandi per gli sviluppatori di Visual Studio o PowerShell.
  • .NET MSBuild: questa versione di MSBuild è integrata negli strumenti da riga di comando di .NET Core. Viene eseguito in .NET Core. Visual Studio non richiama direttamente questa versione di MSBuild. Supporta solo progetti compilati con Microsoft.NET.Sdk.

Se si vuole condividere il codice tra .NET Framework e qualsiasi altra implementazione di .NET, ad esempio .NET Core, la libreria deve avere come destinazione .NET Standard 2.0e si vuole eseguire in Visual Studio, che viene eseguito in .NET Framework. .NET Framework non supporta .NET Standard 2.1.

Scegliere la versione dell'API MSBuild a cui fare riferimento

Quando si compila un'attività personalizzata, è necessario fare riferimento alla versione dell'API MSBuild (Microsoft.Build.*) corrispondente alla versione minima di Visual Studio e/o all'SDK .NET che si prevede di supportare. Ad esempio, per supportare gli utenti in Visual Studio 2019, è necessario eseguire la compilazione con MSBuild 16.11.

Creare l'attività personalizzata MSBuild AppSettingStronglyTyped

Il primo passaggio consiste nel creare l'attività personalizzata MSBuild. Le informazioni su come scrivere un'attività personalizzata di MSBuild possono essere utili per comprendere i passaggi seguenti. Un'attività personalizzata di MSBuild è una classe che implementa l'interfaccia ITask.

  1. Aggiungere un riferimento alla pacchetto Microsoft.Build.Utilities.Core NuGet e quindi creare una classe denominata AppSettingStronglyTyped derivata da Microsoft.Build.Utilities.Task.

  2. Aggiungere tre proprietà. Queste proprietà definiscono i parametri dell'attività impostata dagli utenti quando usano l'attività in un progetto 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; }
    

    L'attività elabora i SettingFiles e genera una classe SettingNamespaceName.SettingClassName. La classe generata avrà un set di costanti in base al contenuto del file di testo.

    L'output dell'attività deve essere una stringa che fornisce il nome file del codice generato:

    // The filename where the class was generated
    [Output]
    public string ClassNameFile { get; set; }
    
  3. Quando si crea un'attività personalizzata, si eredita da Microsoft.Build.Utilities.Task. Per implementare l'attività, eseguire l'override del metodo Execute(). Il metodo Execute restituisce true se l'attività ha esito positivo e false in caso contrario. Task implementa Microsoft.Build.Framework.ITask e fornisce implementazioni predefinite di alcuni membri ITask e offre anche alcune funzionalità di registrazione. È importante restituire lo stato del log per diagnosticare e risolvere i problemi dell'attività, soprattutto se si verifica un problema e l'attività deve restituire un risultato di errore (false). In caso di errore, la classe segnala l'errore chiamando 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 Task consente la restituzione di false, che indica un fallimento, senza indicare all'utente cosa sia andato storto. È consigliabile restituire !Log.HasLoggedErrors anziché un codice booleano e registrare un errore quando si verifica un errore.

Errori di log

La procedura consigliata per la registrazione degli errori consiste nel fornire dettagli, ad esempio il numero di riga e un codice di errore distinto durante la registrazione di un errore. Il codice seguente analizza il file di input di testo e usa il metodo TaskLoggingHelper.LogError con il numero di riga nel file di testo che ha generato l'errore.

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

Usando le tecniche illustrate nel codice precedente, gli errori nella sintassi del file di input di testo vengono visualizzati come errori di compilazione con informazioni diagnostiche utili:

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)

Quando si rilevano le eccezioni nell'attività, usare il metodo TaskLoggingHelper.LogErrorFromException. In questo modo si migliorerà l'output degli errori, ad esempio ottenendo lo stack di chiamate in cui è stata generata l'eccezione.

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'implementazione degli altri metodi che usano questi input per compilare il testo per il file di codice generato non viene visualizzata qui; vedere AppSettingStronglyTyped.cs nel repository di esempio.

Il codice di esempio genera codice C# durante il processo di compilazione. L'attività è simile a qualsiasi altra classe C#, quindi, al termine di questa esercitazione, è possibile personalizzarla e aggiungere qualsiasi funzionalità necessaria per il proprio scenario.

Generare un'applicazione console e usare l'attività personalizzata

In questa sezione creerai un'app console standard di .NET Core che utilizza il task.

Importante

È importante evitare di generare un'attività personalizzata MSBuild nello stesso processo MSBuild che lo utilizzerà. Il nuovo progetto deve trovarsi in una soluzione di Visual Studio completamente diversa, oppure il nuovo progetto utilizza una DLL pregenerata e rilocata dall'output standard.

  1. Creare il progetto console .NET MSBuildConsoleExample in una nuova soluzione di Visual Studio.

    Il modo normale per distribuire un'attività è tramite un pacchetto NuGet, ma durante lo sviluppo e il debug, è possibile includere tutte le informazioni su .props e .targets direttamente nel file di progetto dell'applicazione e quindi passare al formato NuGet quando si distribuisce l'attività ad altri.

  2. Modificare il file di progetto per consumare l'attività di generazione del codice. L'elenco di codice in questa sezione mostra il file di progetto modificato dopo aver fatto riferimento all'attività, impostando i parametri di input per l'attività e scrivendo le destinazioni per la gestione delle operazioni di pulizia e ricompilazione in modo che il file di codice generato venga rimosso come previsto.

    Le attività vengono registrate usando l'elemento UsingTask (MSBuild). L'elemento UsingTask registra l'attività; indica a MSBuild il nome dell'attività e come individuare ed eseguire l'assembly che contiene la classe di attività. Il percorso dell'assembly è relativo al file di progetto.

    Il PropertyGroup contiene le definizioni di proprietà che corrispondono alle proprietà definite nell'attività. Queste proprietà vengono impostate usando gli attributi e il nome dell'attività viene usato come nome dell'elemento.

    TaskName è il nome del task a cui fare riferimento dall'assemblaggio. Questo attributo deve usare sempre namespace completamente specificati. AssemblyFile è il percorso del file dell'assembly.

    Per richiamare l'attività, aggiungere l'attività alla destinazione appropriata, in questo caso GenerateSetting.

    Il target ForceGenerateOnRebuild si occupa delle operazioni di pulizia e ricostruzione eliminando il file generato. È programmato per essere eseguito dopo la destinazione CoreClean impostando l'attributo AfterTargets su 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>
    

    Nota

    Anziché eseguire l'override di una destinazione, ad esempio CoreClean, questo codice usa un altro modo per ordinare le destinazioni (BeforeTarget e AfterTarget). I progetti in stile SDK hanno un'importazione implicita di destinazioni dopo l'ultima riga del file di progetto; ciò significa che non è possibile eseguire l'override delle destinazioni predefinite a meno che non si specifichino manualmente le importazioni. Vedere Eseguire l'override delle destinazioni predefinite.

    Gli attributi Inputs e Outputs consentono a MSBuild di essere più efficienti fornendo informazioni per le compilazioni incrementali. Le date degli input vengono confrontate con gli output per verificare se la destinazione deve essere eseguita o se l'output della build precedente può essere riutilizzato.

  3. Creare il file di testo di input con l'estensione definita da individuare. Usando l'estensione predefinita, creare MyValues.mysettings nella radice, con il contenuto seguente:

    Greeting:string:Hello World!
    
  4. Compila di nuovo e il file dovrebbe essere generato e compilato. Controllare la cartella del progetto per il file MySetting.generated.cs.

  5. La classe MySetting si trova nello spazio dei nomi errato, quindi ora apportare una modifica per usare lo spazio dei nomi dell'app. Aprire il file di progetto e aggiungere il codice seguente:

    <PropertyGroup>
        <SettingNamespace>MSBuildConsoleExample</SettingNamespace>
    </PropertyGroup>
    
  6. Ricompilare di nuovo e osservare che la classe si trova nello spazio dei nomi MSBuildConsoleExample. In questo modo, è possibile ridefinire il nome della classe generata (SettingClass), i file di estensione di testo (SettingExtensionFile) da usare come input e il percorso (RootFolder) di essi, se si desidera.

  7. Apri Program.cs e modifica la stringa fissa 'Hello World!!' alla costante definita dall'utente:

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

Eseguire il programma; stampa il messaggio di saluto dalla classe generata.

(Facoltativo) Registrare gli eventi durante il processo di compilazione

È possibile compilare usando un comando della riga di comando. Passare alla cartella del progetto. Si userà l'opzione -bl (log binario) per generare un log binario. Il log binario avrà informazioni utili per sapere cosa sta succedendo durante il processo di compilazione.

# 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

Entrambi i comandi generano un file di log msbuild.binlog, che può essere aperto con MSBuild Binary e Structured Log Viewer. L'opzione /t:rebuild significa eseguire l'obiettivo di ricompilazione. Forzerà la rigenerazione del file di codice generato.

Felicitazioni! Hai creato un'attività che genera codice e l'hai usata in una compilazione.

Creare un pacchetto dell'attività per la distribuzione

Se è sufficiente usare l'attività personalizzata in alcuni progetti o in una singola soluzione, l'utilizzo dell'attività come assembly non elaborato potrebbe essere sufficiente, ma il modo migliore per preparare l'attività per usarla altrove o condividerla con altri utenti è come pacchetto NuGet.

I pacchetti di attività MSBuild presentano alcune differenze principali rispetto ai pacchetti NuGet della libreria:

  • Devono aggregare le proprie dipendenze di assembly, invece di esporre tali dipendenze al progetto che utilizza
  • Non confezionano alcun assembly richiesto nella cartella lib/<target framework>, perché ciò causerebbe a NuGet di includere gli assembly in qualsiasi pacchetto che consume il task.
  • Devono solo compilare contro gli assembly Microsoft.Build - in fase di esecuzione, questi verranno forniti dall'effettivo motore MSBuild e quindi non è necessario includerli nel pacchetto.
  • Generano un file .deps.json speciale che consente a MSBuild di caricare le dipendenze dell'attività (in particolare le dipendenze native) in modo coerente

Per raggiungere tutti questi obiettivi, è necessario apportare alcune modifiche al file di progetto standard sopra e oltre quelle con cui si ha familiarità.

Creare un pacchetto NuGet

La creazione di un pacchetto NuGet è il modo consigliato per distribuire l'attività personalizzata ad altri utenti.

Preparare la generazione del pacchetto

Per prepararsi a generare un pacchetto NuGet, apportare alcune modifiche al file di progetto per specificare i dettagli che descrivono il pacchetto. Il file di progetto iniziale creato è simile al codice seguente:

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

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

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

</Project>

Per generare un pacchetto NuGet, aggiungere il codice seguente per impostare le proprietà per il pacchetto. È possibile visualizzare un elenco completo delle proprietà di MSBuild supportate nella documentazione di 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 proprietà copyLocalLockFileAssemblies è necessaria per assicurarsi che le dipendenze vengano copiate nella directory di output.

Contrassegnare le dipendenze come private

Le dipendenze dell'attività MSBuild devono essere inserite nel pacchetto; non possono essere espressi come normali riferimenti al pacchetto. Il pacchetto non esporrà dipendenze regolari agli utenti esterni. Per eseguire questa operazione, è necessario contrassegnare gli assembly come privati e incorporarli effettivamente nel pacchetto generato. Per questo esempio, presupponiamo che la tua attività dipenda da Microsoft.Extensions.DependencyInjection, quindi aggiungi un PackageReference a Microsoft.Extensions.DependencyInjection nella versione 6.0.0.

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

Contrassegnare ora tutte le dipendenze di questo progetto Task, sia PackageReference che ProjectReference con l'attributo PrivateAssets="all". In questo modo NuGet non esporrà affatto queste dipendenze ai progetti che le consumano. Per altre informazioni sul controllo degli asset di dipendenza , vedere la documentazione di 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>

Includere le dipendenze nel pacchetto

È inoltre necessario incorporare le risorse runtime delle nostre dipendenze nel pacchetto Task. Ci sono due parti: un obiettivo MSBuild che aggiunge le nostre dipendenze al gruppo di elementi BuildOutputInPackage e alcune proprietà che controllano il layout di quegli elementi BuildOutputInPackage. Per altre informazioni su questo processo , vedere la documentazione di 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>

Non includere l'assembly Microsoft.Build.Utilities.Core

Come illustrato in precedenza, questa dipendenza verrà fornita da MSBuild in fase di esecuzione, quindi non è necessario aggregarla nel pacchetto. A tale scopo, aggiungere l'attributo ExcludeAssets="Runtime" al PackageReference

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

Generare e incorporare un file deps.json

Il file deps.json può essere usato da MSBuild per assicurarsi che vengano caricate le versioni corrette delle dipendenze. È necessario aggiungere alcune proprietà di MSBuild per fare in modo che il file venga generato, perché non viene generato per impostazione predefinita per le librerie. Aggiungere quindi un obiettivo per includerlo nell'output del pacchetto, in modo analogo a come è stato fatto per le dipendenze del pacchetto.

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

Includere le proprietà e le destinazioni di MSBuild in un pacchetto

Per informazioni generali su questa sezione, leggere le informazioni sulle proprietà e sulle destinazioni e su come includere proprietà e destinazioni in un pacchetto NuGet.

In alcuni casi, potrebbe essere necessario aggiungere destinazioni o proprietà di compilazione personalizzate nei progetti che utilizzano il pacchetto, ad esempio l'esecuzione di uno strumento personalizzato o un processo durante la compilazione. A tale scopo, inserire i file nel modulo <package_id>.targets o <package_id>.props all'interno della cartella build del progetto.

I file nella directory principale del progetto nella cartella di compilazione sono considerati adatti a tutti i framework di destinazione.

In questa sezione collegherai l'implementazione dell'attività nei file .props e .targets, che verranno inclusi nel nostro pacchetto NuGet e caricati automaticamente da un progetto di riferimento.

  1. Nel file di progetto dell'attività AppSettingStronglyTyped.csprojaggiungere il codice seguente:

    <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. Creare una cartella di compilazione e in tale cartella aggiungere due file di testo: AppSettingStronglyTyped.props e AppSettingStronglyTyped.targets. AppSettingStronglyTyped.props viene importato anticipatamente in Microsoft.Common.props, e le proprietà definite in un secondo momento non sono disponibili. Quindi, evitare di fare riferimento a proprietà non ancora definite; risulterebbero vuote.

    Directory.Build.targets viene importato da Microsoft.Common.targets dopo l'importazione di file .targets da pacchetti NuGet. Può quindi sovrascrivere proprietà e obiettivi definiti nella maggior parte della logica di compilazione o impostare proprietà per tutti i progetti indipendentemente da ciò che impostano i singoli progetti. Consulta l'ordine di importazione .

    appSettingStronglyTyped.props include l'attività e definisce alcune proprietà con valori predefiniti:

    <?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. Il file AppSettingStronglyTyped.props viene incluso automaticamente quando viene installato il pacchetto. Il client ha quindi l'attività disponibile e alcuni valori predefiniti. Tuttavia, non viene mai usato. Per mettere in azione questo codice, definire alcune destinazioni nel file AppSettingStronglyTyped.targets, che verranno incluse automaticamente anche quando il pacchetto viene installato:

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

    Il primo passaggio è la creazione di un ItemGroup, che rappresenta i file di testo (potrebbe essere più di uno) da leggere e sarà un parametro dell'attività. Sono disponibili valori predefiniti per il percorso e l'estensione in cui viene cercata, ma è possibile eseguire l'override dei valori che definiscono le proprietà nel file di progetto MSBuild client.

    Definire quindi due destinazioni MSBuild. Noi estendiamo il processo MSBuild, sovrascrivendo i target predefiniti.

    • BeforeCompile: l'obiettivo è chiamare l'attività personalizzata per generare la classe e includere la classe da compilare. Le attività in questa destinazione vengono inserite prima che venga eseguita la compilazione principale. I campi di input e di output sono correlati alla compilazione incrementale . Se tutti gli elementi di output sono up-to-date, MSBuild ignora la destinazione. Questa compilazione incrementale dell'obiettivo può migliorare significativamente le prestazioni dei build. Un elemento viene considerato up-to-date se il file di output è la stessa età o più recente rispetto al file o ai file di input.

    • AfterClean: l'obiettivo è eliminare il file di classe generato dopo che si verifica una pulizia generale. Le attività in questa destinazione vengono inserite dopo l'invocazione della funzionalità principale di pulizia. Forza la ripetizione del passaggio di generazione del codice quando viene eseguito l'obiettivo di ricompilazione.

Generare il pacchetto NuGet

Per generare il pacchetto NuGet, è possibile usare Visual Studio (fare clic con il pulsante destro del mouse sul nodo del progetto in Esplora soluzioni e selezionare Pack). È anche possibile farlo usando la riga di comando. Passare alla cartella in cui è presente il file di progetto attività AppSettingStronglyTyped.csproj ed eseguire il comando seguente:

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

Felicitazioni! È stato generato un pacchetto NuGet denominato \AppSettingStronglyTyped\AppSettingStronglyTyped\AppSettingStronglyTyped.1.0.0.nupkg.

Il pacchetto ha un'estensione .nupkg ed è un file ZIP compresso. È possibile aprirlo con uno strumento ZIP. I file .target e .props si trovano nella cartella build. Il file .dll si trova nella cartella lib\netstandard2.0\. Il file AppSettingStronglyTyped.nuspec si trova nella directory principale.

(Facoltativo) Supportare il multitargeting

È consigliabile considerare la possibilità di supportare sia Full (.NET Framework) che Core (incluse le distribuzioni di .NET 5 e versioni successive) di MSBuild per supportare la base utente più ampia possibile.

Per i progetti .NET SDK "normali", il multitargeting indica l'impostazione di più oggetti TargetFrameworks nel file di progetto. Quando si esegue questa operazione, le compilazioni verranno attivate sia per entrambi i TargetFrameworkMonikers e i risultati complessivi possono essere inseriti in un unico artefatto.

Non è la storia completa per MSBuild. MSBuild ha due veicoli di spedizione principali: Visual Studio e .NET SDK. Si tratta di ambienti di runtime molto diversi; una viene eseguita nel runtime di .NET Framework e altre esecuzioni in CoreCLR. Ciò significa che, mentre il codice può essere destinato a netstandard2.0, la logica delle attività può avere differenze in base al tipo di runtime DI MSBuild attualmente in uso. In pratica, poiché sono presenti così tante nuove API in .NET 5.0 e versioni successive, è opportuno configurare sia il codice sorgente dell'attività MSBuild per multipli TargetFrameworkMonikers che la logica di destinazione MSBuild per multipli tipi di runtime MSBuild.

Modifiche necessarie per il multitargeting

Per impostare come destinazione più TargetFrameworkMonikers (TFM):

  1. Modificare il file di progetto in modo da usare le net472 e le net6.0 TFM (quest'ultimo può cambiare in base al livello SDK di destinazione). È possibile impostare come destinazione netcoreapp3.1 fino a quando .NET Core 3.1 non esce dal supporto. In questo caso, la struttura delle cartelle del pacchetto cambia da tasks/ a tasks/<TFM>/.

    <TargetFrameworks>net472;net6.0</TargetFrameworks>
    
  2. Aggiorna i file .targets per usare il TFM corretto per caricare le attività. Il TFM richiesto cambierà in base al TFM .NET scelto sopra, ma per un progetto destinato a net472 e net6.0, ci sarebbe una proprietà come la seguente:

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

Questo codice usa la proprietà MSBuildRuntimeType come proxy per l'ambiente di hosting attivo. Dopo aver impostato questa proprietà, è possibile usarla nel UsingTask per caricare il AssemblyFilecorretto:

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

Passaggi successivi

Molte attività comportano la chiamata di un eseguibile. In alcuni scenari è possibile usare l'attività Exec, ma se le limitazioni dell'attività Exec sono un problema, è anche possibile creare un'attività personalizzata. L'esercitazione seguente illustra entrambe le opzioni con uno scenario di generazione del codice più realistico: creazione di un'attività personalizzata per generare codice client per un'API REST.

In alternativa, informazioni su come testare un'attività personalizzata.