Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
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. Este exemplo demonstra como usar o MSBuild para lidar com as operações limpas e de recompilação. O exemplo também mostra como dar suporte ao build incremental, de modo 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 gama 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 do .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 dentro do Visual Studio. É executado no .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. Ele é executado no .NET Core. O Visual Studio não invoca diretamente essa versão do MSBuild. Ele só dá suporte a projetos criados 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.
Escolha a versão da API do MSBuild para fazer referência
Ao compilar uma tarefa personalizada, você deve referenciar a versão da API do MSBuild (Microsoft.Build.*) que corresponde à versão mínima do Visual Studio e/ou ao SDK do .NET que você espera dar suporte. Por exemplo, para dar suporte aos usuários no Visual Studio 2019, você deve compilar com o MSBuild 16.11.
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.
Adicione uma referência ao pacote Microsoft.Build.Utilities.Core NuGet e crie uma classe chamada AppSettingStronglyTyped derivada de Microsoft.Build.Utilities.Task.
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 os 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; }Ao criar uma tarefa personalizada, você herda de Microsoft.Build.Utilities.Task. Para implementar a tarefa, substitua o método Execute(). O método
Executeretornatruese a tarefa for bem-sucedida efalsecaso contrário.Taskimplementa Microsoft.Build.Framework.ITask e fornece implementações padrão de alguns membrosITaske, além disso, adiciona algumas funcionalidades de log. É importante registrar o status no log para diagnosticar e resolver problemas da tarefa, especialmente se ocorrer um problema e a tarefa deverá retornar um resultado de erro (false). Com o 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.HasLoggedErrorsem vez de um código booliano e registrar um erro quando algo der errado.
Erros de 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, 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 terminar este tutorial, você poderá personalizá-la e adicionar qualquer funcionalidade necessária para seu próprio cenário.
Gere um aplicativo de console e utilize a tarefa personalizada
Nesta seção, você criará um aplicativo de console padrão do .NET Core que usa a tarefa.
Importante
É importante evitar a geração de uma tarefa personalizada do MSBuild no mesmo processo do MSBuild que a consumirá. O novo projeto deve estar em uma solução completamente diferente do Visual Studio, ou o novo projeto usará uma dll pré-gerada e relocada da saída padrão.
Crie o projeto de console do .NET MSBuildConsoleExample em 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
.propse.targetsdiretamente no arquivo de projeto do aplicativo e, em seguida, mover para o formato NuGet ao distribuir a tarefa para outras pessoas.Modifique o arquivo de projeto para consumir a tarefa de geração de código. O exemplo 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 definir os alvos para lidar com operações de limpeza e recompilação, para que o arquivo de código gerado seja removido conforme o esperado.
As tarefas são registradas usando o elemento UsingTask (MSBuild). O elemento
UsingTaskregistra 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
PropertyGroupconté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
ForceGenerateOnRebuildmanipula as operações de limpeza e recompilação excluindo o arquivo gerado. Ele é definido para ser executado após o destinoCoreCleandefinindo o atributoAfterTargetscomoCoreClean.<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
Em vez de substituir um destino como
CoreClean, esse código usa outra maneira de ordenar os destinos (BeforeTarget e AfterTarget). 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. Confira Substituir destinos predefinidos.Os atributos
InputseOutputsajudam o MSBuild a ser mais eficiente fornecendo informações para builds incrementais. As datas das entradas são comparadas com as saídas para determinar se o alvo precisa ser executado ou se a saída do build anterior pode ser reutilizada.Crie o arquivo de texto de entrada com a extensão a ser identificada. Usando a extensão padrão, crie
MyValues.mysettingsna raiz, com o seguinte conteúdo:Greeting:string:Hello World!Compile novamente e o arquivo gerado deve ser criado e construído. Verifique a pasta do projeto para o arquivo MySetting.generated.cs.
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 seguinte código:
<PropertyGroup> <SettingNamespace>MSBuildConsoleExample</SettingNamespace> </PropertyGroup>Recompile novamente e observe que a classe está no namespace
MSBuildConsoleExample. Dessa forma, você pode redefinir o nome da classe gerada (SettingClass), os arquivos de extensão de texto (SettingExtensionFile) a serem usados como entrada e o local (RootFolder) deles, se desejar.Abra
Program.cse altere o 'Olá, Mundo!!' codificado para a constante definida pelo usuário:static void Main(string[] args) { Console.WriteLine(MySetting.Greeting); }
Execute o programa; ele imprimirá 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 and Structured Log Viewer . A opção /t:rebuild significa executar o destino de recompilação. Isso 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ê precisar usar sua tarefa personalizada apenas em alguns projetos ou em uma única solução, utilizar a tarefa como um assembly simples pode ser suficiente. No entanto, a melhor forma de preparar sua tarefa para usá-la em outros lugares ou compartilhar com outras pessoas é transformá-la em 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 de
.deps.jsonespecial que ajuda o MSBuild a carregar as dependências da Tarefa (especialmente dependências nativas) de forma consistente
Para atingir 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 Pack:
<PropertyGroup>
...
<IsPackable>true</IsPackable>
<Version>1.0.0</Version>
<Title>AppSettingStronglyTyped</Title>
<Authors>Your author name</Authors>
<Description>Generates a strongly typed setting class base on a text file.</Description>
<PackageTags>MyTags</PackageTags>
<Copyright>Copyright ©Contoso 2022</Copyright>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
...
</PropertyGroup>
A propriedade CopyLocalLockFileAssemblies é necessária para garantir que as dependências sejam copiadas para o diretório de saída.
Marcar dependências como privadas
As dependências da tarefa MSBuild devem ser empacotadas dentro do pacote; eles não podem ser expressos como referências de pacote normais. 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. Para este exemplo, presumiremos que sua tarefa depende do funcionamento de Microsoft.Extensions.DependencyInjection, portanto, adicione um PackageReference a 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>
<!-- Suppress NuGet warning NU5128. -->
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
...
</PropertyGroup>
...
<!-- This is the target we defined above. It's purpose is to add all of our PackageReference and ProjectReference's runtime assets to our package output. -->
<Target
Name="CopyProjectReferencesToPackage"
DependsOnTargets="ResolveReferences">
<ItemGroup>
<!-- The TargetPath is the path inside the package that the source file will be placed. This is already precomputed in the ReferenceCopyLocalPaths items' DestinationSubPath, so reuse it here. -->
<BuildOutputInPackage
Include="@(ReferenceCopyLocalPaths)"
TargetPath="%(ReferenceCopyLocalPaths.DestinationSubPath)" />
</ItemGroup>
</Target>
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 empacotá-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 de 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 alvos do 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, talvez você queira adicionar destinos de build personalizados ou propriedades em projetos que consomem seu pacote, como executar uma ferramenta personalizada ou um processo durante o build. Faça isso colocando arquivos no formulário <package_id>.targets ou <package_id>.props dentro da pasta build no 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.
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>Crie uma pasta build e, nessa pasta, adicione dois arquivos de texto:
AppSettingStronglyTyped.propse AppSettingStronglyTyped.targets.AppSettingStronglyTyped.propsé importado no início de Microsoft.Common.props e as propriedades definidas posteriormente não estão disponíveis para ele. Portanto, evite fazer referência a propriedades que ainda não foram definidas; eles seriam avaliados como vazios.Directory.Build.targets é importado do Microsoft.Common.targets depois de importar os arquivos
.targetsdos pacotes do NuGet. Portanto, ele pode substituir propriedades e alvos definidos em grande parte da lógica de build ou definir propriedades para todos os projetos, independentemente do que os projetos individuais definem. Confira 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> <!--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>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 arquivoAppSettingStronglyTyped.targets, que também será incluído automaticamente quando o pacote for instalado:<?xml version="1.0" encoding="utf-8" ?> <Project> <!--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á parte do nosso parâmetro 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 alvos 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. Essa compilação incremental do alvo pode melhorar significativamente o desempenho de suas compilações. Um item é considerado up-to-date se o arquivo de saída for da mesma idade ou mais recente que o arquivo de entrada ou os arquivos.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 no do Gerenciador de Soluçõese 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
Considere dar suporte a distribuições do MSBuild Full (.NET Framework) e Core (incluindo .NET 5 e posterior) para dar suporte à base de usuários mais ampla possível.
Para projetos 'normais' do SDK do .NET, multitargeting 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.
Essa não é a história completa para o MSBuild. O MSBuild tem dois veículos de transporte primários: o Visual Studio e o SDK do .NET. São ambientes de runtime muito diferentes; uma é executada no runtime do .NET Framework e outra é executada no CoreCLR. O que 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 atualmente em uso. 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):
Altere o arquivo de projeto para usar os TFMs
net472enet6.0(o último pode ser alterado com base no número de versão do SDK que você deseja almejar). Talvez você queira direcionarnetcoreapp3.1até que o .NET Core 3.1 fique sem suporte. Quando você faz isso, a estrutura da pasta do pacote muda detasks/paratasks/<TFM>/.<TargetFrameworks>net472;net6.0</TargetFrameworks>Atualize seus arquivos
.targetspara 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 projetonet472enet6.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 AssemblyFilecorreto:
<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 o código do cliente para uma API REST.
Ou saiba como testar uma tarefa personalizada.