Tutorial: Creación de una tarea personalizada para la generación de código

En este tutorial, creará una tarea personalizada de MSBuild en C# que controla la generación de código y, a continuación, usará la tarea en una compilación. En este ejemplo se muestra cómo usar MSBuild para controlar las operaciones de limpieza y recompilación. En el ejemplo también se muestra cómo admitir la compilación incremental, para que el código se genere solo cuando los archivos de entrada han cambiado. Las técnicas demostradas son aplicables a una amplia gama de escenarios de generación de código. Los pasos también muestran el uso de NuGet para empaquetar la tarea para la distribución, y el tutorial incluye un paso opcional para usar el visor BinLog para mejorar la experiencia de solución de problemas.

Requisitos previos

Debe comprender los conceptos de MSBuild, como tareas, destinos y propiedades. Vea Conceptos de MSBuild.

Los ejemplos requieren MSBuild, que se instala con Visual Studio, pero también se puede instalar por separado. Vea Descargar MSBuild sin Visual Studio.

Introducción al ejemplo de código

En el ejemplo se usa un archivo de texto de entrada que contiene los valores que se deben establecer y se crea un archivo de código de C# con código que crea estos valores. Aunque se trata de un ejemplo sencillo, se pueden aplicar las mismas técnicas básicas a escenarios de generación de código más complejos.

En este tutorial, creará una tarea personalizada de MSBuild denominada AppSettingStronglyTyped. La tarea leerá un conjunto de archivos de texto y cada archivo con líneas con el formato siguiente:

propertyName:type:defaultValue

El código genera una clase de C# con todas las constantes. Un problema debe detener la compilación y proporcionar al usuario información suficiente para diagnosticar el problema.

El código de ejemplo completo de este tutorial se encuentra en Tarea personalizada: generación de código en el repositorio de ejemplos de .NET en GitHub.

Creación del proyecto AppSettingStronglyTyped

Cree una biblioteca de clases de .NET Standard. El marco debe ser .NET Standard 2.0.

Tenga en cuenta la diferencia entre MSBuild completo (el que Visual Studio usa) y MSBuild portátil, el incluido en la línea de comandos de .NET Core.

  • MSBuild completo: esta versión de MSBuild suele estar dentro de Visual Studio. Se ejecuta en .NET Framework. Visual Studio lo usa al ejecutar Compilar en la solución o el proyecto. Esta versión también está disponible en un entorno de línea de comandos, como el Símbolo del sistema para desarrolladores de Visual Studio, o bien PowerShell.
  • MSBuild para .NET: esta versión de MSBuild se incluye en la línea de comandos de .NET Core. Se ejecuta en .NET Core. Visual Studio no invoca directamente esta versión de MSBuild. Solo admite proyectos que se compilan mediante Microsoft.NET.Sdk.

Si desea compartir código entre .NET Framework y cualquier otra implementación de .NET, como .NET Core, la biblioteca debe tener como destino .NET Standard 2.0, y desea realizar la ejecución dentro de Visual Studio, que se ejecuta en .NET Framework. .NET Framework no es compatible con .NET Standard 2.1.

Creación de la tarea personalizada AppSettingStronglyTyped de MSBuild

El primer paso es crear la tarea personalizada de MSBuild. La información sobre cómo escribir una tarea personalizada de MSBuild puede ayudarle a comprender los pasos siguientes. Una tarea personalizada de MSBuild es una clase que implementa la interfaz ITask.

  1. Agregue una referencia al paquete NuGet Microsoft.Build.Utilities.Core y, a continuación, cree una clase denominada AppSettingStronglyTyped derivada de Microsoft.Build.Utilities.Task.

  2. Agregue tres propiedades. Estas propiedades definen los parámetros de la tarea que los usuarios establecen cuando usan la tarea en un proyecto de cliente:

     //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 tarea procesa SettingFiles y genera una clase SettingNamespaceName.SettingClassName. La clase generada tendrá un conjunto de constantes basadas en el contenido del archivo de texto.

    La salida de la tarea debe ser una cadena que proporciona el nombre de archivo del código generado:

     // The filename where the class was generated
     [Output]
     public string ClassNameFile { get; set; }
    
  3. Al crear una tarea personalizada, hereda de Microsoft.Build.Utilities.Task. Para implementar la tarea, reemplace el método Execute(). El método Execute devuelve true si la tarea se realiza correctamente; de lo contrario, devuelve false. Task implementa Microsoft.Build.Framework.ITask y proporciona implementaciones predeterminadas de algunos miembros ITask y, además, proporciona cierta funcionalidad de registro. Es importante generar el estado en el registro para diagnosticar y solucionar problemas de la tarea, especialmente si se produce un problema y la tarea debe devolver un resultado de error (false). En caso de error, la clase señala el error mediante una llamada a 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;
     }
    

    Task API permite devolver false, lo que indica un error, sin indicar al usuario lo que salió mal. Es mejor devolver !Log.HasLoggedErrors en lugar de un código booleano y registrar un error cuando algo sale mal.

Registro de errores

El procedimiento recomendado es proporcionar detalles como el número de línea y un código de error distinto al registrar un error. El código siguiente analiza el archivo de entrada de texto y usa el método TaskLoggingHelper.LogError con el número de línea en el archivo de texto que produjo el error.

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

Con las técnicas que se muestran en el código anterior, los errores en la sintaxis del archivo de entrada de texto se muestran como errores de compilación con información de diagnóstico útil:

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)

Cuando se detectan excepciones en la tarea, use el método TaskLoggingHelper.LogErrorFromException. Esto mejorará la salida de error, por ejemplo, al obtener la pila de llamadas donde se produjo la excepción.

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

La implementación de los otros métodos que usan estas entradas para compilar el texto para el archivo de código generado no se muestra aquí; vea AppSettingStronglyTyped.cs en el repositorio de ejemplo.

El código de ejemplo genera código de C# durante el proceso de compilación. La tarea es como cualquier otra clase de C#, por lo que cuando haya terminado con este tutorial, puede personalizarla y agregar la funcionalidad necesaria para su propio escenario.

Generación de una aplicación de consola y uso de la tarea personalizada

En esta sección, creará una aplicación de consola estándar de .NET Core que usa la tarea.

Importante

Es importante evitar la generación de una tarea personalizada de MSBuild en el mismo proceso de MSBuild que la va a consumir. El nuevo proyecto debe estar en una solución de Visual Studio completa diferente, o el nuevo proyecto usa un archivo DLL generado previamente y reubicado a partir de la salida estándar.

  1. Cree el proyecto de consola de .NET MSBuildConsoleExample en una nueva solución de Visual Studio.

    La manera normal de distribuir una tarea es a través de un paquete NuGet, pero durante el desarrollo y la depuración, puede incluir toda la información sobre .props y .targets directamente en el archivo del proyecto de la aplicación y, a continuación, pasar al formato de NuGet al distribuir la tarea a otros usuarios.

  2. Modifique el archivo del proyecto para consumir la tarea de generación de código. La lista de códigos de esta sección muestra el archivo del proyecto modificado después de hacer referencia a la tarea, establecer los parámetros de entrada de la tarea y escribir los destinos para controlar las operaciones de limpieza y recompilación para que el archivo de código generado se quite como se esperaría.

    Las tareas se registran mediante el elemento UsingTask (MSBuild). El elemento UsingTask registra la tarea; indica a MSBuild el nombre de la tarea y cómo buscar y ejecutar el ensamblado que contiene la clase de tarea. La ruta de acceso del ensamblado es relativa al archivo del proyecto.

    PropertyGroup contiene las definiciones de propiedad que corresponden a las propiedades definidas en la tarea. Estas propiedades se establecen mediante atributos, y el nombre de la tarea se usa como nombre del elemento.

    TaskName es el nombre de la tarea a la que se va a hacer referencia desde el ensamblado. Este atributo siempre debe usar espacios de nombres totalmente especificados. AssemblyFile es la ruta de acceso del archivo del ensamblado.

    Para invocar la tarea, agregue la tarea al destino adecuado, en este caso GenerateSetting.

    El destino ForceGenerateOnRebuild controla las operaciones de limpieza y recompilación mediante la eliminación del archivo generado. Se establece para ejecutarse después del destino CoreClean estableciendo el atributo AfterTargets en 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

    En lugar de reemplazar un destino como CoreClean, este código usa otra manera de ordenar los destinos (BeforeTarget y AfterTarget). Los proyectos de estilo SDK tienen una importación implícita de destinos después de la última línea del archivo del proyecto; esto significa que no puede reemplazar los destinos predeterminados a menos que especifique las importaciones manualmente. Vea Reemplazar destinos predefinidos.

    Los atributos Inputs y Outputs ayudan a que MSBuild sea más eficaz proporcionando información para las compilaciones incrementales. Las fechas de las entradas se comparan con las salidas para ver si es necesario ejecutar el destino o si se puede reutilizar la salida de la compilación anterior.

  3. Cree el archivo de texto de entrada con la extensión definida que se va a detectar. Con la extensión predeterminada, cree MyValues.mysettings en la raíz, con el siguiente contenido:

     Greeting:string:Hello World!
    
  4. Vuelva a realizar la compilación, y el archivo generado debe crearse y compilarse. Compruebe la carpeta del proyecto para el archivo MySetting.generated.cs.

  5. La clase MySetting está en el espacio de nombres incorrecto, así que, ahora realice un cambio para usar el espacio de nombres de la aplicación. Abra el archivo del proyecto y agregue el siguiente código:

     <PropertyGroup>
     	<SettingNamespace>MSBuildConsoleExample</SettingNamespace>
     </PropertyGroup>
    
  6. Vuelva a recompilar y observe que la clase está en el espacio de nombres MSBuildConsoleExample. De este modo, puede volver a definir el nombre de clase generado (SettingClass), los archivos de extensión de texto (SettingExtensionFile) que se usarán como entrada y su ubicación (RootFolder) si lo desea.

  7. Abra Program.cs y cambie la codificación "Hola mundo" a la constante definida por el usuario:

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

Ejecute el programa; imprimirá el saludo de la clase generada.

(Opcional) Registro de eventos durante el proceso de compilación

Es posible compilar mediante un comando de línea de comandos. Vaya a la carpeta del proyecto. Usará la opción -bl (registro binario) para generar un registro binario. El registro binario tendrá información útil para saber lo que sucede durante el proceso de compilación.

# 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

Ambos comandos generan un archivo de registro msbuild.binlog, que se puede abrir con MSBuild Binary and Structured Log Viewer. La opción /t:rebuild sirve para ejecutar el destino de recompilación. Forzará la regeneración del archivo de código generado.

Felicidades. Ha creado una tarea que genera código y la ha usado en una compilación.

Empaquetado de la tarea para la distribución

Si solo necesita usar la tarea personalizada en algunos proyectos o en una única solución, consumir la tarea como un ensamblado sin procesar podría ser todo lo que necesita, pero la mejor manera de preparar la tarea para usarla en otro lugar o compartirla con otros es como un paquete NuGet.

Los paquetes de tareas de MSBuild tienen algunas diferencias clave con respecto a los paquetes NuGet de las bibliotecas:

  • Tienen que agrupar sus propias dependencias de ensamblado, en lugar de exponer esas dependencias al proyecto de consumo.
  • No empaquetan los ensamblados necesarios en una carpeta lib/<target framework>, ya que eso haría que NuGet incluyera los ensamblados en cualquier paquete que consuma la tarea.
  • Solo necesitan compilar en los ensamblados Microsoft.Build: en el entorno de ejecución, el motor de MSBuild real proporcionará estos elementos, por lo que no es necesario incluirlos en el paquete.
  • Generan un archivo .deps.json especial que ayuda a MSBuild a cargar las dependencias de la tarea (especialmente las dependencias nativas) de forma coherente.

Para lograr todos estos objetivos, debe realizar algunos cambios en el archivo de proyecto estándar anterior y más allá de aquellos con los que puede estar familiarizado.

Creación de un paquete NuGet

La creación de un paquete de NuGet es la manera recomendada de distribuir la tarea personalizada a otros usuarios.

Preparación para generar el paquete

Para prepararse para generar un paquete NuGet, realice algunos cambios en el archivo del proyecto para especificar los detalles que describen el paquete. El archivo del proyecto inicial que creó es similar al código siguiente:

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

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

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

</Project>

Para generar un paquete NuGet, agregue el código siguiente para establecer las propiedades del paquete. Puede ver una lista completa de las propiedades de MSBuild admitidas en la documentación del paquete:

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

Marcación de las dependencias como privadas

Las dependencias de la tarea de MSBuild deben empaquetarse dentro del paquete; no se pueden expresar como referencias de paquete normales. El paquete no expondrá ninguna dependencia normal a usuarios externos. Esto requiere dos pasos: marcar los ensamblados como privados e insertarlos realmente en el paquete generado. En este ejemplo, se supone que la tarea depende de que Microsoft.Extensions.DependencyInjection funcione, por lo que debe agregar un elemento PackageReference a Microsoft.Extensions.DependencyInjection en la versión 6.0.0.

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

Ahora, marque todas las dependencias de este proyecto de tareas, tanto PackageReference y ProjectReference con el atributo PrivateAssets="all". Esto indicará a NuGet que no exponga estas dependencias a los proyectos de consumo en absoluto. Puede obtener más información sobre cómo controlar los recursos de dependencia en la documentación de 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>

Agrupación de dependencias en el paquete

También debe insertar los recursos del entorno de ejecución de nuestras dependencias en el paquete de tareas. Esto tiene dos partes: un objetivo MSBuild que agrega nuestras dependencias al ItemGroup BuildOutputInPackage, y unas cuantas propiedades que controlan el diseño de esos elementos BuildOutputInPackage. Puede obtener más información sobre este proceso en la documentación de 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>

No inclusión del ensamblado Microsoft.Build.Utilities.Core en la agrupación

Como se ha explicado anteriormente, esta dependencia se proporcionará mediante MSBuild en el entorno de ejecución, por lo que no es necesario incluirla en el paquete. Para ello, agregue el atributo ExcludeAssets="Runtime" a PackageReference.

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

Generación e inserción de un archivo deps.json

MSBuild puede usar el archivo deps.json para asegurarse de que se cargan las versiones correctas de las dependencias. Deberá agregar algunas propiedades de MSBuild para que se genere el archivo, ya que no se genera de forma predeterminada para las bibliotecas. A continuación, agregue un destino para incluirlo en la salida del paquete, de forma similar a como hizo para las dependencias del paquete.

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

Inclusión de propiedades y destinos de MSBuild en un paquete

Para obtener información general sobre esta sección, lea sobre las propiedades y los destinos y, a continuación, sobre cómo incluir propiedades y destinos en un paquete NuGet.

En algunos casos, es posible que quiera agregar destinos de compilación personalizados o propiedades en proyectos en los que se consume el paquete, como la ejecución de una herramienta o un proceso personalizado durante la compilación. Para ello, coloque los archivos en el formato <package_id>.targets o <package_id>.props dentro de la carpeta build del proyecto.

Los archivos de la carpeta build raíz del proyecto se consideran adecuados para todas las plataformas de destino.

En esta sección, conectará la implementación de la tarea en los archivos .props y .targets, que se incluirá en nuestro paquete NuGet y se cargará automáticamente desde un proyecto de referencia.

  1. En el archivo del proyecto de la tarea, AppSettingStronglyTyped.csproj, agregue el código siguiente:

     <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. Cree una carpeta build y, en ella, agregue dos archivos de texto: AppSettingStronglyTyped.props y AppSettingStronglyTyped.targets. AppSettingStronglyTyped.props se importa rápidamente en Microsoft.Common.props, y las propiedades definidas después no están disponibles ahí. Por tanto, evite hacer referencia a propiedades que todavía no están definidas; se evaluarían como vacías.

    Directory.Build.targets se importa desde Microsoft.Common.targets después de importar los archivos .targets de paquetes NuGet. Por lo tanto, puede invalidar las propiedades y los destinos definidos en la mayor parte de la lógica de compilación, o bien, establecer las propiedades de todos los proyectos independientemente de lo que establezcan los proyectos individuales. Vea Orden de importación.

    AppSettingStronglyTyped.props incluye la tarea y define algunas propiedades con valores predeterminados:

     <?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. El archivo AppSettingStronglyTyped.props se incluye automáticamente cuando se instala el paquete. A continuación, el cliente tiene la tarea disponible y algunos valores predeterminados. Sin embargo, nunca se usa. Para poner este código en acción, defina algunos destinos en el archivo AppSettingStronglyTyped.targets, que también se incluirá automáticamente cuando se instale el paquete:

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

    El primer paso es crear un ItemGroup, que representa los archivos de texto (podría ser más de uno) para leer, y formará parte de nuestro parámetro de tarea. Hay valores predeterminados para la ubicación y la extensión donde buscamos, pero puede reemplazar los valores que definen las propiedades en el archivo del proyecto de MSBuild del cliente.

    A continuación, defina dos destinos de MSBuild. Ampliamos el proceso de MSBuild, reemplazando los destinos predefinidos:

    • BeforeCompile: el objetivo es llamar a la tarea personalizada para generar la clase e incluir la clase que se va a compilar. Las tareas de este destino se insertan antes de que se realice la compilación principal. Los campos de entrada y salida están relacionados con la compilación incremental. Si todos los elementos de salida están actualizados, MSBuild omite el destino. Esta compilación incremental del destino puede mejorar significativamente el rendimiento de las compilaciones. Un elemento se considera actualizado si su archivo de salida tiene una antigüedad igual o inferior a la de su archivo o archivos de entrada.

    • AfterClean: el objetivo es eliminar el archivo de clase generado después de que se produzca una limpieza general. Las tareas de este destino se insertan después de invocar la funcionalidad de limpieza básica. Obliga a repetir el paso de generación de código cuando se ejecuta el destino Recompilar.

Generación del paquete NuGet

Para generar el paquete NuGet, puede usar Visual Studio (haga clic con el botón derecho en el nodo del proyecto en Explorador de soluciones y seleccione Empaquetar). También puede hacerlo mediante la línea de comandos. Vaya a la carpeta donde se encuentra el archivo del proyecto AppSettingStronglyTyped.csproj de la tarea y ejecute el siguiente comando:

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

Felicidades. Ha generado un paquete NuGet denominado \AppSettingStronglyTyped\AppSettingStronglyTyped\AppSettingStronglyTyped.1.0.0.nupkg.

El paquete tiene una extensión .nupkg y es un archivo ZIP comprimido. Puede abrirlo con una herramienta zip. Los archivos .target y .props se encuentran en la carpeta build. El archivo .dll está en la carpeta lib\netstandard2.0\. El archivo AppSettingStronglyTyped.nuspec está en el nivel raíz.

(Opcional) Admisión de compatibilidad con múltiples versiones (multi-targeting)

Considere la posibilidad de admitir las distribuciones de MSBuild Full (.NET Framework) y Core (incluidas .NET 5 y versiones posteriores) para admitir la base de usuarios más amplia posible.

En el caso de los proyectos "normales" del SDK de .NET, la compatibilidad con múltiples versiones (multi-targeting) implica establecer varios elementos TargetFrameworks en el archivo del proyecto. Al hacerlo, se desencadenan compilaciones para ambos TargetFrameworkMonikers y los resultados generales se pueden empaquetar como un único artefacto.

Pero eso no es todo en lo referente a MSBuild. MSBuild tiene dos vehículos de envío principales: Visual Studio y el SDK de .NET. Se trata de entornos runtime muy diferentes; uno se ejecuta en el runtime de .NET Framework y el otro en CoreCLR. Esto significa que, aunque el código puede tener como destino netstandard2.0, la lógica de la tarea puede tener diferencias en función del tipo de runtime de MSBuild que se esté usando actualmente. En la práctica, dado que hay tantas API nuevas en .NET 5.0 y versiones posteriores, tiene sentido tener en cuenta tanto el código fuente de la tarea de MSBuild para varios TargetFrameworkMonikers como la lógica de destino de MSBuild para varios tipos de runtime de MSBuild.

Cambios necesarios en multitarget

Para establecer como destino varios TargetFrameworkMonikers (TFM):

  1. Cambie el archivo de proyecto para usar los TFM net472 y net6.0 (este último puede cambiar en función del nivel de SDK de destino). Es posible que quiera tener como destino netcoreapp3.1 hasta que .NET Core 3.1 deje de tener soporte técnico. Al hacerlo, la estructura de carpetas del paquete cambia de tasks/ a tasks/<TFM>/.

    <TargetFrameworks>net472;net6.0</TargetFrameworks>
    
  2. Actualice los archivos .targets para usar el TFM correcto para cargar las tareas. El TFM necesario cambiará en función del TFM de .NET que eligió anteriormente, pero para un proyecto que tenga como destino net472 y net6.0, tendría una propiedad como la siguiente:

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

Este código usa la propiedad MSBuildRuntimeType como proxy para el entorno de hospedaje activo. Una vez establecida esta propiedad, puede usarla en UsingTask para cargar AssemblyFile:

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

Pasos siguientes

Muchas tareas implican llamar a un archivo ejecutable. En algunos escenarios, puede usar la tarea Exec, pero si las limitaciones de la tarea Exec son un problema, también puede crear una tarea personalizada. El siguiente tutorial le guía por ambas opciones con un escenario de generación de código más realista: crear una tarea personalizada para generar código de cliente para una API de REST.

O bien, aprenda a probar una tarea personalizada.