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 si creerà un'attività personalizzata MSBuild denominata AppSettingStronglyTyped. L'attività leggerà un set di file di testo e ogni file con righe con il 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 in 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 usa questa opzione quando si esegue La compilazione nella soluzione o nel 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 è in bundle nella 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.0 e si vuole eseguire in Visual Studio, che viene eseguita in .NET Framework. .NET Framework non supporta .NET Standard 2.1.

Creare l'attività personalizzata AppSettingStronglyTyped MSBuild

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 al pacchetto NuGet Microsoft.Build.Utilities.Core 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 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 Execute() metodo . Il Execute metodo 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 ITask membri e fornisce 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 attività consente la restituzione di false, che indica un errore, senza indicare all'utente cosa è andato storto. È consigliabile restituire !Log.HasLoggedErrors anziché un codice booleano e registrare un errore quando si verifica un errore.

Errori del 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 TaskLoggingHelper.LogError metodo 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 eccezioni nell'attività, usare il TaskLoggingHelper.LogErrorFromException metodo . 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'app console e usare l'attività personalizzata

In questa sezione si creerà un'app console .NET Core standard che usa l'attività.

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 diversa completa oppure il nuovo progetto usa una DLL pregenerata e ri-individuata dall'output standard.The new project should be in a complete different Visual Studio solution, or the new project use a dll pre-generate and re-located from the standard output.

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

  2. Modificare il file di progetto per utilizzare 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à e indica a MSBuild il nome dell'attività e come individuare ed eseguire l'assembly contenente la classe dell'attività. Il percorso dell'assembly è relativo al file di progetto.

    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 dell'attività a cui fare riferimento dall'assembly. Questo attributo deve usare sempre spazi dei nomi completamente specificati. AssemblyFile è il percorso del file dell'assembly.

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

    La destinazione ForceGenerateOnRebuild gestisce le operazioni di pulizia e ricompilazione eliminando il file generato. È impostato per l'esecuzione dopo la CoreClean destinazione 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, CoreCleanad esempio , 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 di destinazioni predefinite.

    Gli Inputs attributi 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 generato deve essere creato 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 MSBuildConsoleExample nomi . In questo modo, è possibile ridefinire il nome della classe generata (SettingClass), i file di estensione del testo (SettingExtensionFile) da usare come input e il percorso (RootFolder) di essi, se si preferisce.

  7. Aprire Program.cs e modificare l'hardcoded '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 msbuild.binlogdi log, che può essere aperto con MSBuild Binary e Structured Log Viewer. L'opzione indica l'esecuzione /t:rebuild della destinazione di ricompilazione. Forza la rigenerazione del file di codice generato.

Complimenti. È stata creata un'attività che genera codice e la si usa 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 includono assembly necessari in una lib/<target framework> cartella, perché nuGet include gli assembly in qualsiasi pacchetto che utilizza l'attività
  • Devono solo essere compilati in base agli assembly Microsoft.Build: in fase di esecuzione questi verranno forniti dal motore MSBuild effettivo e quindi non è necessario includere nel pacchetto
  • Generano un file speciale .deps.json 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>
	...
</PropertyGroup>

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 si presuppone che l'attività funzioni Microsoft.Extensions.DependencyInjection , quindi aggiungere un PackageReference a alla Microsoft.Extensions.DependencyInjection 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 PackageReferenceProjectReference con l'attributo PrivateAssets="all" . In questo modo NuGet non esporrà queste dipendenze all'utilizzo di progetti. 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>

Creare un bundle delle dipendenze nel pacchetto

È anche necessario incorporare gli asset di runtime delle dipendenze nel pacchetto attività. A questo scopo sono presenti due parti: una destinazione MSBuild che aggiunge le dipendenze a BuildOutputInPackage ItemGroup e alcune proprietà che controllano il layout di tali BuildOutputInPackage elementi. 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>
	...
</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 aggregare 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" a per esso 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 una destinazione per includerla 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 di base 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 personalizzata nei progetti che utilizzano il pacchetto, ad esempio l'esecuzione di uno strumento o processo personalizzato durante la compilazione. A tale scopo, inserire i file nel modulo <package_id>.targets o <package_id>.props all'interno della build cartella nel progetto.

I file nella cartella di compilazione radice del progetto sono considerati adatti per tutti i framework di destinazione.

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

  1. Nel file di progetto dell'attività AppSettingStronglyTyped.csproj aggiungere 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 nelle prime fasi di Microsoft.Common.props e le proprietà definite in un secondo momento non sono disponibili. Quindi, evitare di fare riferimento a proprietà non ancora definite; restituiscono vuoto.

    Directory.Build.targets viene importato da Microsoft.Common.targets dopo l'importazione di .targets file da pacchetti NuGet. Può quindi eseguire l'override di proprietà e destinazioni definite nella maggior parte della logica di compilazione o impostare proprietà per tutti i progetti indipendentemente dal set dei singoli progetti. Vedere 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 inserire questo codice in azione, definire alcune destinazioni nel file AppSettingStronglyTyped.targets , che verranno incluse automaticamente anche quando viene installato il pacchetto:

     <?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. Il processo MSBuild viene esteso, sostituendo le destinazioni predefinite:

    • 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 output sono correlati alla compilazione incrementale. Se tutti gli elementi di output sono aggiornati, MSBuild ignora la destinazione. Questa compilazione incrementale della destinazione può migliorare significativamente le prestazioni delle compilazioni. Un elemento viene considerato aggiornato se il relativo file di output è stato creato nello stesso momento dei relativi file di input o è più recente.

    • AfterClean: l'obiettivo è eliminare il file di classe generato dopo un'operazione pulita generale. Le attività in questa destinazione vengono inserite dopo che viene richiamata la funzionalità pulita principale. Forza la ripetizione del passaggio di generazione del codice quando viene eseguita la destinazione 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 dell'attività AppSettingStronglyTyped.csproj ed eseguire il comando seguente:

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

Complimenti. È 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 .target file e .props si trovano nella build cartella . Il .dll file si trova nella lib\netstandard2.0\ cartella . Il AppSettingStronglyTyped.nuspec file è a livello radice.

(Facoltativo) Supportare il multitargeting

È consigliabile considerare la possibilità di supportare sia Full le distribuzioni di MSBuild (.NET Framework) sia Core (incluse .NET 5 e versioni successive) 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 TargetFrameworkMoniker che per 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 successiva, è opportuno configurare sia il codice sorgente dell'attività MSBuild per più targetFrameworkMoniker che la logica di destinazione MSBuild per più tipi di runtime MSBuild.

Modifiche necessarie per il multitarget

Per impostare come destinazione più TargetFrameworkMonikers (TFM):

  1. Modificare il file di progetto in modo da usare i net472 TFM e net6.0 (quest'ultimo può cambiare in base al livello SDK di destinazione). È possibile scegliere come destinazione netcoreapp3.1 fino a quando .NET Core 3.1 non è più supportato. Quando si esegue questa operazione, la struttura delle cartelle del pacchetto cambia da tasks/ a tasks/<TFM>/.

    <TargetFrameworks>net472;net6.0</TargetFrameworks>
    
  2. Aggiornare i .targets file per usare il TFM corretto per caricare le attività. Il TFM richiesto cambierà in base a quanto scelto in precedenza da .NET TFM, ma per un progetto destinato net472 a e net6.0, si avrà una proprietà simile alla seguente:

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

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

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