Tutorial: Criar uma tarefa personalizada para a geração de código

Neste tutorial, você criará uma tarefa personalizada no MSBuild em C# que manipula a geração de código e, em seguida, usará a tarefa em um build. Esse exemplo demonstra como usar o MSBuild para lidar com as operações de limpeza e recompilação. O exemplo também mostra como dar suporte ao build incremental, para que o código seja gerado somente quando os arquivos de entrada forem alterados. As técnicas demonstradas são aplicáveis a uma ampla variedade de cenários de geração de código. As etapas também mostram o uso do NuGet para empacotar a tarefa para distribuição, e o tutorial inclui uma etapa opcional para usar o visualizador do BinLog para melhorar a experiência de solução de problemas.

Pré-requisitos

Você deve ter uma compreensão dos conceitos do MSBuild, como tarefas, destinos e propriedades. Confira Conceitos do MSBuild.

Os exemplos exigem o MSBuild, que é instalado com o Visual Studio, mas também pode ser instalado separadamente. Confira Baixar o MSBuild sem o Visual Studio.

Introdução ao exemplo de código

O exemplo usa um arquivo de texto de entrada que contém valores a serem definidos e cria um arquivo de código C# com código que cria esses valores. Embora esse seja um exemplo simples, as mesmas técnicas básicas podem ser aplicadas a cenários de geração de código mais complexos.

Neste tutorial, você criará uma tarefa personalizada do MSBuild chamada AppSettingStronglyTyped. A tarefa lerá um conjunto de arquivos de texto e cada arquivo com linhas com o seguinte formato:

propertyName:type:defaultValue

O código gera uma classe C# com todas as constantes. Um problema deve interromper o build e fornecer ao usuário informações suficientes para diagnosticar o problema.

O código de exemplo completo deste tutorial está em Tarefa personalizada – geração de código no repositório de exemplos do .NET no GitHub.

Criar o projeto AppSettingStronglyTyped

Crie uma biblioteca de classes .NET Standard. A estrutura deve ser .NET Standard 2.0.

Observe a diferença entre o MSBuild completo (aquele que o Visual Studio usa) e o MSBuild portátil, aquele agrupado na Linha de Comando do .NET Core.

  • MSBuild completo: essa versão do MSBuild geralmente reside no Visual Studio. É executado em .NET Framework. O Visual Studio usa isso quando você executa Build em sua solução ou projeto. Essa versão também está disponível em um ambiente de linha de comando, como o Prompt de Comando do Desenvolvedor do Visual Studio ou o PowerShell.
  • .NET MSBuild: esta versão do MSBuild é agrupada na linha de comando do .NET Core. Ela é executada no .NET Core. O Visual Studio não invoca diretamente essa versão do MSBuild. Ele só dá suporte a projetos que são compilados usando o Microsoft.NET.Sdk.

se você quiser compartilhar código entre .NET Framework e qualquer outra implementação do .NET, como o .NET Core, sua biblioteca deverá ter como destino o .NET Standard 2.0 e você deseja executar dentro do Visual Studio, que é executado no .NET Framework. O .NET Framework não dá suporte ao .NET Standard 2.1.

Criar a tarefa personalizada do MSBuild AppSettingStronglyTyped

A primeira etapa é criar a tarefa personalizada do MSBuild. Informações sobre como escrever uma tarefa personalizada do MSBuild podem ajudar a entender as etapas a seguir. Uma tarefa personalizada do MSBuild é uma classe que implementa a interface ITask.

  1. Adicione uma referência ao pacote NuGet Microsoft.Build.Utilities.Core e crie uma classe chamada AppSettingStronglyTyped derivada de Microsoft.Build.Utilities.Task.

  2. Adicione três propriedades. Essas propriedades definem os parâmetros da tarefa que os usuários definem quando usam a tarefa em um projeto 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; }
    

    A tarefa processa SettingFiles e gera uma classe SettingNamespaceName.SettingClassName. A classe gerada terá um conjunto de constantes com base no conteúdo do arquivo de texto.

    A saída da tarefa deve ser uma cadeia de caracteres que fornece o nome do arquivo do código gerado:

     // The filename where the class was generated
     [Output]
     public string ClassNameFile { get; set; }
    
  3. Ao criar uma tarefa personalizada, você herda de Microsoft.Build.Utilities.Task. Para implementar a tarefa, substitua o método Execute(). O método Execute retornará true se a tarefa for bem-sucedida, e false se não for. Task implementa Microsoft.Build.Framework.ITask e fornece implementações padrão de alguns membros ITask e, além disso, fornece algumas funcionalidades de registro em log. É importante gerar o status para o log para diagnosticar e solucionar problemas da tarefa, especialmente se ocorrer um problema e a tarefa precisar retornar um resultado de erro (false). Em caso de erro, a classe sinaliza o erro chamando 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;
     }
    

    A API de Tarefa permite retornar false, indicando falha, sem indicar ao usuário o que deu errado. É melhor retornar !Log.HasLoggedErrors em vez de um código booliano e registrar um erro quando algo der errado.

Erros do log

A melhor prática ao registrar erros em log é fornecer detalhes como o número de linha e um código de erro distinto ao registrar um erro em log. O código a seguir analisa o arquivo de entrada de texto e usa o método TaskLoggingHelper.LogError com o número de linha no arquivo de texto que produziu o erro.

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 as técnicas mostradas no código anterior, os erros na sintaxe do arquivo de entrada de texto aparecem como erros de build com informações úteis de diagnóstico:

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)

Ao capturar exceções em sua tarefa, use o método TaskLoggingHelper.LogErrorFromException. Isso melhorará a saída de erro, por exemplo, obtendo a pilha de chamadas em que a exceção foi gerada.

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

A implementação dos outros métodos que usam essas entradas para criar o texto para o arquivo de código gerado não é mostrada aqui; consulte AppSettingStronglyTyped.cs no repositório de exemplo.

O código de exemplo gera código C# durante o processo de build. A tarefa é como qualquer outra classe C#, portanto, quando você terminar este tutorial, poderá personalizá-la e adicionar qualquer funcionalidade necessária para seu próprio cenário.

Gerar um aplicativo de console e usar a tarefa personalizada

Nesta seção, você criará um aplicativo de console padrão do .NET Core que usa a tarefa.

Importante

É importante evitar gerar uma tarefa personalizada do MSBuild no mesmo processo do MSBuild que a consumirá. O novo projeto deve estar em uma solução completa e diferente do Visual Studio ou o novo projeto usar uma dll pré-gerada e reloqueada da saída padrão.

  1. Crie o projeto do Console do .NET MSBuildConsoleExample em uma nova Solução do Visual Studio.

    A maneira normal de distribuir uma tarefa é por meio de um pacote NuGet, mas durante o desenvolvimento e a depuração, você pode incluir todas as informações sobre .props e .targets diretamente no arquivo de projeto do aplicativo e, em seguida, mover para o formato NuGet ao distribuir a tarefa para outras pessoas.

  2. Modifique o arquivo de projeto para consumir a tarefa de geração de código. A listagem de código nesta seção mostra o arquivo de projeto modificado depois de referenciar a tarefa, definir os parâmetros de entrada para a tarefa e gravar os destinos para lidar com operações limpas e de recompilação para que o arquivo de código gerado seja removido como esperado.

    Registre as tarefas usando o elemento UsingTask (MSBuild). O elemento UsingTask registra a tarefa; ele informa ao MSBuild o nome da tarefa e como localizar e executar o assembly que contém a classe de tarefa. O caminho do assembly é relativo ao arquivo de projeto.

    O PropertyGroup contém as definições de propriedade que correspondem às propriedades definidas na tarefa. Essas propriedades são definidas usando atributos e o nome da tarefa é usado como o nome do elemento.

    TaskName é o nome da tarefa de referência do assembly. Esse atributo sempre deve usar namespaces totalmente especificados. AssemblyFile é o caminho do arquivo do assembly.

    Para invocar a tarefa, adicione a tarefa ao destino apropriado, nesse caso GenerateSetting.

    O destino ForceGenerateOnRebuild manipula as operações de limpeza e recompilação excluindo o arquivo gerado. Ele é definido para ser executado após o destino CoreClean definindo o atributo AfterTargets como 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>
    

    Observação

    Em vez de substituir um destino como CoreClean, esse código usa outra maneira de ordenar os destinos (BeforeTarget e AfterTarget). Os projetos no estilo SDK têm uma importação implícita de destinos após a última linha do arquivo de projeto; isso significa que você não pode substituir os destinos padrão, a menos que especifique suas importações manualmente. Consulte Substituir destinos predefinidos.

    Os atributos Inputs e Outputs ajudam o MSBuild a ser mais eficiente fornecendo informações para builds incrementais. As datas das entradas são comparadas com as saídas para ver se o destino precisa ser executado ou se a saída do build anterior pode ser reutilizado.

  3. Crie o arquivo de texto de entrada com a extensão definida para ser descoberta. Usando a extensão padrão, crie MyValues.mysettings na raiz, com o seguinte conteúdo:

     Greeting:string:Hello World!
    
  4. Compile novamente e o arquivo gerado deve ser criado e criado. Verifique a pasta do projeto para o arquivo MySetting.generated.cs.

  5. A classe MySetting está no namespace errado, portanto, agora faça uma alteração para usar o namespace do aplicativo. Abra o arquivo de projeto e adicione o código a seguir:

     <PropertyGroup>
     	<SettingNamespace>MSBuildConsoleExample</SettingNamespace>
     </PropertyGroup>
    
  6. Recompile novamente e observe que a classe está no namespace MSBuildConsoleExample. Dessa forma, você pode redefinir o nome de classe gerado (SettingClass), os arquivos de extensão de texto (SettingExtensionFile) a serem usados como entrada e o local (RootFolder) deles, se desejar.

  7. Abra Program.cs e altere o 'Hello, World!!' codificado para a constante definida pelo usuário:

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

Execute o programa; ele mostrará a saudação da classe gerada.

(Opcional) Eventos de log durante o processo de build

É possível compilar usando um comando de linha de comando. Navegue até a pasta do projeto. Você usará a opção -bl (log binário) para gerar um log binário. O log binário terá informações úteis para saber o que está acontecendo durante o processo de build.

# Using dotnet MSBuild (run core environment)
dotnet build -bl

# or full MSBuild (run on net framework environment; this is used by Visual Studio)
msbuild -bl

Ambos os comandos geram um arquivo de log msbuild.binlog, que pode ser aberto com o MSBuild Binary e o Visualizador de Log Estruturado. A opção /t:rebuild significa executar o destino de recompilação. Ela forçará a regeneração do arquivo de código gerado.

Parabéns! Você criou uma tarefa que gera código e a usou em um build.

Empacotar a tarefa para distribuição

Se você só precisar usar sua tarefa personalizada em alguns projetos ou em uma única solução, consumir a tarefa como um assembly bruto pode ser o suficiente, mas a melhor maneira de preparar sua tarefa para usá-la em outro lugar ou compartilhá-la com outras pessoas é como um pacote NuGet.

Os pacotes de tarefas do MSBuild têm algumas diferenças importantes em relação aos pacotes NuGet da biblioteca:

  • Eles precisam agrupar suas próprias dependências de assembly, em vez de expor essas dependências ao projeto de consumo
  • Eles não empacotam assemblies necessários para uma pasta lib/<target framework>, pois isso faria com que o NuGet incluísse os assemblies em qualquer pacote que consuma a tarefa
  • Eles só precisam ser compilados nos assemblies Microsoft.Build. Em runtime, eles serão fornecidos pelo mecanismo real do MSBuild e, portanto, não precisam ser incluídos no pacote
  • Eles geram um arquivo especial .deps.json que ajuda o MSBuild a carregar as dependências da Tarefa (especialmente dependências nativas) de maneira consistente

Para cumprir todas essas metas, você precisa fazer algumas alterações no arquivo de projeto padrão acima e além daquelas com as quais você pode estar familiarizado.

Criar um pacote NuGet

Criar um pacote NuGet é a maneira recomendada de distribuir sua tarefa personalizada para outras pessoas.

Preparar para gerar o pacote

Para se preparar para gerar um pacote NuGet, faça algumas alterações no arquivo de projeto para especificar os detalhes que descrevem o pacote. O arquivo de projeto inicial criado se assemelha ao seguinte código:

<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 gerar um pacote NuGet, adicione o código a seguir para definir as propriedades do pacote. Você pode ver uma lista completa das propriedades do MSBuild com suporte na documentação do Pacote:

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

Marcar dependências como privadas

As dependências da tarefa do MSBuild devem ser empacotadas dentro do pacote; elas não podem ser expressas como referências normais de pacote. O pacote não exporá nenhuma dependência regular a usuários externos. São necessárias duas etapas para isso: marcar seus assemblies como privados e, na verdade, inseri-los no pacote gerado. Neste exemplo, vamos supor que sua tarefa depende de Microsoft.Extensions.DependencyInjection para funcionar, portanto, adicione um PackageReference para Microsoft.Extensions.DependencyInjection na versão 6.0.0.

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

Agora, marque todas as dependências deste projeto de Tarefa, PackageReference e ProjectReference com o atributo PrivateAssets="all". Isso instruirá o NuGet a não expor essas dependências ao consumo de projetos. Você pode ler mais sobre como controlar ativos de dependência na documentação do 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>

Agrupar dependências no pacote

Você também deve inserir os ativos de runtime de nossas dependências no pacote de tarefas. Há duas partes para isso: um destino do MSBuild que adiciona nossas dependências ao ItemGroup BuildOutputInPackage e algumas propriedades que controlam o layout desses itens BuildOutputInPackage. Você pode saber mais sobre esse processo na documentação do 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>

Não agrupe o assembly Microsoft.Build.Utilities.Core

Conforme discutido acima, essa dependência será fornecida pelo próprio MSBuild em runtime, portanto, não precisamos agrupá-la no pacote. Para fazer isso, adicione o atributo ExcludeAssets="Runtime" ao PackageReference para ele

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

Gerar e inserir um arquivo deps.json

O arquivo deps.json pode ser usado pelo MSBuild para garantir que as versões corretas de suas dependências sejam carregadas. Você precisará adicionar algumas propriedades do MSBuild para fazer com que o arquivo seja gerado, pois ele não é gerado por padrão para bibliotecas. Em seguida, adicione um destino para incluí-lo em nossa saída de pacote, da mesma forma que você fez para nossas dependências de pacote.

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

Incluir propriedades e destinos de MSBuild em um pacote

Para obter informações sobre esta seção, leia sobre propriedades e destinos e, em seguida, como incluir propriedades e destinos em um pacote NuGet.

Em alguns casos, convém adicionar destinos ou propriedades de build personalizados a projetos que consomem o pacote, como a execução de uma ferramenta personalizada ou um processo durante o build. Faça isso colocando arquivos no formato <package_id>.targets ou <package_id>.props dentro da pasta build do projeto.

Arquivos na pasta build raiz são considerados adequados para todas as estruturas de destino.

Nesta seção, você conectará a implementação da tarefa em arquivos .props e .targets, que serão incluídos em nosso pacote NuGet e carregados automaticamente de um projeto de referência.

  1. No arquivo de projeto da tarefa, AppSettingStronglyTyped.csproj, adicione o seguinte código:

     <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. Crie uma pasta build e, nessa pasta, adicione dois arquivos de texto: AppSettingStronglyTyped.props e AppSettingStronglyTyped.targets. Directory.Build.props é importado no início no Microsoft.Common.props e as propriedades definidas posteriormente não ficam disponíveis para ele. Portanto, evite referenciar propriedades que ainda não foram definidas; elas serão avaliadas como vazias.

    Directory.Build.targets é importado do Microsoft.Common.targets depois de importar os arquivos .targets dos pacotes do NuGet. Portanto, ele pode substituir propriedades e destinos definidos na maior parte da lógica de build ou definir propriedades para todos os seus projetos, independentemente do que os projetos individuais definem. Consulte ordem de importação.

    AppSettingStronglyTyped.props inclui a tarefa e define algumas propriedades com valores padrão:

     <?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. O arquivo AppSettingStronglyTyped.props é incluído automaticamente quando o pacote é instalado. Em seguida, o cliente tem a tarefa disponível e alguns valores padrão. No entanto, ele nunca é usado. Para colocar esse código em ação, defina alguns destinos no arquivo AppSettingStronglyTyped.targets, que também será incluído automaticamente quando o pacote for instalado:

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

    A primeira etapa é a criação de um ItemGroup, que representa os arquivos de texto (pode ser mais de um) para ler e será alguns de nossos parâmetros de tarefa. Há valores padrão para o local e a extensão em que procuramos, mas você pode substituir os valores que definem as propriedades no arquivo de projeto do MSBuild do cliente.

    Em seguida, defina dois destinos do MSBuild. Estendemos o processo do MSBuild, substituindo destinos predefinidos:

    • BeforeCompile: a meta é chamar a tarefa personalizada para gerar a classe e incluir a classe a ser compilada. As tarefas nesse destino são inseridas antes que a compilação principal seja concluída. O parâmetro de entrada e saída está relacionado ao build incremental. Se todos os itens de saída estiverem atualizados, o MSBuild ignorará o destino. Esse build incremental do destino pode melhorar significativamente o desempenho de seus builds. Um item será considerado atualizado se seu arquivo de saída tiver a mesma idade ou for mais recente que seu arquivo ou arquivos de entrada.

    • AfterClean: a meta é excluir o arquivo de classe gerado após uma limpeza geral acontecer. As tarefas nesse destino são inseridas depois que a funcionalidade de limpeza principal é invocada. Ela força a etapa de geração de código a ser repetida quando o destino Recompilação é executado.

Gerar o pacote NuGet

Para gerar o pacote NuGet, você pode usar o Visual Studio (clique com o botão direito do mouse no nó do projeto em Gerenciador de Soluções e selecione Pacote). Você também pode fazer isso usando a linha de comando. Navegue até a pasta em que o arquivo de projeto de tarefa AppSettingStronglyTyped.csproj está presente e execute o seguinte comando:

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

Parabéns! Você gerou um pacote NuGet chamado \AppSettingStronglyTyped\AppSettingStronglyTyped\AppSettingStronglyTyped.1.0.0.nupkg.

O pacote tem uma extensão .nupkg e é um arquivo zip compactado. Você pode abri-lo com uma ferramenta zip. Os arquivos .target e .props estão na pasta build. O arquivo .dll está na pasta lib\netstandard2.0\. O arquivo AppSettingStronglyTyped.nuspec está no nível raiz.

(Opcional) Suporte a vários destinos

Você deve considerar o suporte às distribuições do MSBuild Full (.NET Framework) e Core (incluindo o .NET 5 e posterior) para dar suporte à base de usuários mais ampla possível.

Para projetos 'normais' do SDK do .NET, ter vários destinos significa definir vários TargetFrameworks no arquivo de projeto. Quando você fizer isso, os builds serão disparados para TargetFrameworkMonikers e os resultados gerais poderão ser empacotados como um único artefato.

Mas isso não é tudo quando se trata do MSBuild. O MSBuild tem dois veículos de envio primários: o Visual Studio e o SDK do .NET. São ambientes de runtime muito diferentes; uma é executada no runtime .NET Framework e outra é executada no CoreCLR. Isso significa é que, embora seu código possa ser direcionado para netstandard2.0, sua lógica de tarefa pode ter diferenças com base no tipo de runtime do MSBuild que está sendo usado no momento. Na prática, como há muitas APIs novas no .NET 5.0 ou mais, faz sentido ter vários destinos no seu código-fonte de tarefa DO MSBuild para vários TargetFrameworkMonikers, bem como vários destinos da lógica de destino do MSBuild para vários tipos de runtime do MSBuild.

Alterações necessárias para vários destinos

Para direcionar vários TargetFrameworkMonikers (TFM):

  1. Altere o arquivo do seu projeto para usar os TFMs net472 e net6.0 (este último pode ser alterado com base no nível do SDK que você deseja direcionar). Talvez você queira direcionar netcoreapp3.1 até que o .NET Core 3.1 fique sem suporte. Quando você faz isso, a estrutura da pasta do pacote muda de tasks/ para tasks/<TFM>/.

    <TargetFrameworks>net472;net6.0</TargetFrameworks>
    
  2. Atualize seus arquivos .targets para usar o TFM correto para carregar suas tarefas. O TFM necessário será alterado com base no TFM do .NET que você escolheu acima, mas para um direcionamento de projeto net472 e net6.0, você teria uma propriedade como:

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

Esse código usa a propriedade MSBuildRuntimeType como um proxy para o ambiente de hospedagem ativo. Depois que essa propriedade for definida, você poderá usá-la no UsingTask para carregar o AssemblyFile correto:

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

Próximas etapas

Muitas tarefas envolvem chamar um executável. Em alguns cenários, você pode usar a tarefa Exec, mas se as limitações da tarefa Exec forem um problema, você também poderá criar uma tarefa personalizada. O tutorial a seguir explica as duas opções com um cenário de geração de código mais realista: criar uma tarefa personalizada para gerar código de cliente para uma API REST.

Ou saiba como testar uma tarefa personalizada.