Partilhar via


Tutorial: Criar uma tarefa personalizada para 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 uma compilação. Este exemplo demonstra como usar o MSBuild para manipular as operações de limpeza e reconstrução. O exemplo também mostra como dar suporte à compilação 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 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 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. Consulte conceitos do MSBuild.

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

Introdução ao exemplo de código

O exemplo usa um arquivo de texto de entrada contendo 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 irá 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 parar a compilação e dar ao usuário informações suficientes para diagnosticar o problema.

O código de exemplo completo para este tutorial está em tarefa personalizada - geração de código no repositório de exemplos .NET no GitHub.

Criar o projeto AppSettingStronglyTyped

Crie uma biblioteca de classes padrão do .NET. 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 incluído na linha de comando do .NET Core.

  • MSBuild completo: Esta versão do MSBuild geralmente vive dentro do Visual Studio. É executado no .NET Framework. Visual Studio usa isso quando você executa Build em sua solução ou projeto. Esta versão também está disponível em um ambiente de linha de comando, como o Visual Studio Developer Command Prompt ou PowerShell.
  • .NET MSBuild: Esta versão do MSBuild é empacotada na linha de comando do .NET Core. Ele é executado no .NET Core. Visual Studio não invoca diretamente esta versão do MSBuild. Ele só suporta projetos que compilam usando Microsoft.NET.Sdk.

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

Escolha a versão da API do MSBuild para referência

Ao compilar uma tarefa personalizada, você deve fazer referência à versão da API do MSBuild (Microsoft.Build.*) que corresponde à versão mínima do Visual Studio e/ou o SDK do .NET que você espera suportar. Por exemplo, para oferecer suporte a usuários no Visual Studio 2019, você deve criar com base no MSBuild 16.11.

Criar uma tarefa personalizada para o MSBuild: AppSettingStronglyTyped

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

  1. Adicione uma referência ao Microsoft.Build.Utilities.Core pacote NuGet 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 o 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 forneça 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, você substitui o método Execute(). O método Execute retorna true se a tarefa for bem-sucedida e false caso contrário. Task implementa Microsoft.Build.Framework.ITask e fornece implementações padrão de alguns membros do ITask e, adicionalmente, fornece algumas funcionalidades de registro. É importante enviar o status para o log para diagnosticar e solucionar problemas da tarefa, especialmente se ocorrer um problema e a tarefa tiver que retornar um resultado de erro (false). No 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 tarefas permite retornar false, indicando falha, sem indicar ao usuário o que deu errado. É melhor retornar !Log.HasLoggedErrors em vez de um código booleano e registrar um erro quando algo der errado.

Erros de log

A prática recomendada ao registrar erros é fornecer detalhes como o número da linha e um código de erro distinto ao registrar um erro. O código a seguir analisa o arquivo de entrada de texto e usa o método TaskLoggingHelper.LogError com o número da 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 compilação com informações de diagnóstico úteis:

Microsoft (R) Build Engine version 17.2.0 for .NET Framework
Copyright (C) Microsoft Corporation. All rights reserved.

Build started 2/16/2022 10:23:24 AM.
Project "S:\work\msbuild-examples\custom-task-code-generation\AppSettingStronglyTyped\AppSettingStronglyTyped.Test\bin\Debug\net6.0\Resources\testscript-fail.msbuild" on node 1 (default targets).
S:\work\msbuild-examples\custom-task-code-generation\AppSettingStronglyTyped\AppSettingStronglyTyped.Test\bin\Debug\net6.0\Resources\error-prop.setting(1): error APPS0001: Incorrect line format. Valid format prop:type:defaultvalue [S:\work\msbuild-examples\custom-task-code-generation\AppSettingStronglyTyped\AppSettingStronglyTyped.Test\bin\Debug\net6.0\Resources\testscript-fail.msbuild]
Done Building Project "S:\work\msbuild-examples\custom-task-code-generation\AppSettingStronglyTyped\AppSettingStronglyTyped.Test\bin\Debug\net6.0\Resources\testscript-fail.msbuild" (default targets) -- FAILED.

Build FAILED.

"S:\work\msbuild-examples\custom-task-code-generation\AppSettingStronglyTyped\AppSettingStronglyTyped.Test\bin\Debug\net6.0\Resources\testscript-fail.msbuild" (default target) (1) ->
(generateSettingClass target) ->
  S:\work\msbuild-examples\custom-task-code-generation\AppSettingStronglyTyped\AppSettingStronglyTyped.Test\bin\Debug\net6.0\Resources\error-prop.setting(1): error APPS0001: Incorrect line format. Valid format prop:type:defaultvalue [S:\work\msbuild-examples\custom-task-code-generation\AppSettingStronglyTyped\AppSettingStronglyTyped.Test\bin\Debug\net6.0\Resources\testscript-fail.msbuild]

     0 Warning(s)
     1 Error(s)

Quando detetares exceções na tua tarefa, utiliza o método TaskLoggingHelper.LogErrorFromException. Isso melhorará a saída de erro, por exemplo, ao obter a pilha de chamadas onde a exceção ocorreu.

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 construir o texto para o arquivo de código gerado não é mostrada aqui; Veja AppSettingStronglyTyped.cs no exemplo de repositório.

O código de exemplo gera código C# durante o processo de compilação. 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 uma aplicação de console e utilize a tarefa personalizada

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

Importante

É importante evitar gerar uma tarefa personalizada do MSBuild no mesmo processo do MSBuild que vai consumi-la. O novo projeto deve estar em uma solução Visual Studio completamente diferente, ou o novo projeto usar uma dll pré-gerada e realocada a partir da saída padrão.

  1. Crie o projeto de console .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 seu aplicativo e, em seguida, mover para o formato NuGet quando 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 fazer referência à tarefa, definir os parâmetros de entrada para a tarefa e escrever os alvos para lidar com operações de limpeza e reconstrução, para que o arquivo de código gerado seja removido conforme esperado.

    As tarefas são registradas 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 task. 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 a ser referenciada a partir do assembly. Esse atributo sempre deve usar namespaces totalmente especificados. AssemblyFile é o caminho do arquivo do assembly.

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

    O alvo ForceGenerateOnRebuild lida com as operações de limpeza e reconstrução apagando o ficheiro gerado. Ele está 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, este 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 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 compilações 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 da compilação anterior pode ser reutilizada.

  3. Crie o ficheiro de texto de entrada com a extensão definida a 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 construído. Verifique a pasta do projeto para o arquivo MySetting.generated.cs.

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

    <PropertyGroup>
        <SettingNamespace>MSBuildConsoleExample</SettingNamespace>
    </PropertyGroup>
    
  6. Reconstrua novamente e observe que a classe está no namespace MSBuildConsoleExample. Desta 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.

  7. Abra Program.cs e altere o 'Hello World!!' fixo no código. para a constante definida pelo usuário:

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

Executar o programa; ele imprimirá a saudação da classe gerada.

(Opcional) Registrar eventos durante o processo de compilação

É 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 compilação.

# 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 MSBuild Binary and Structured Log Viewer. A opção /t:rebuild significa executar o alvo de reconstruçã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 uma compilação.

Empacotar a tarefa para distribuição

Se precisar usar a tarefa personalizada em apenas alguns projetos ou numa única solução, poderá bastar consumir a tarefa como um assembly simples. No entanto, a melhor forma de preparar a tarefa para ser utilizada noutro lugar ou partilhada com outros é sob a forma de um pacote NuGet.

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

  • Eles têm que agrupar as suas próprias dependências de montagem, em vez de expor essas dependências ao projeto consumidor.
  • Eles não empacotam nenhum assembly necessário para uma pasta lib/<target framework>, pois isso faria com que o NuGet incluísse os assemblies em qualquer pacote que utilize a tarefa.
  • Eles só precisam compilar contra os assemblies Microsoft.Build - durante a execução, estes serão fornecidos pelo mecanismo MSBuild real e, portanto, não é necessário incluí-los no pacote
  • Eles geram um arquivo .deps.json especial que ajuda o MSBuild a carregar as dependências da tarefa (especialmente as dependências nativas) de forma consistente

Para atingir todos esses objetivos, você tem que fazer algumas alterações no arquivo de projeto padrão acima e além daqueles com os 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 que você criou semelhante 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 suportadas 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 sua tarefa MSBuild devem ser empacotadas dentro do pacote; não podem ser expressas como referências normais de pacotes. O pacote não irá expor quaisquer dependências regulares a utilizadores externos. Isso leva duas etapas para realizar: marcar os seus assemblies como privados e efetivamente incorporá-los no pacote gerado. Para este exemplo, assumiremos que a tua tarefa depende de Microsoft.Extensions.DependencyInjection para funcionar, portanto, adiciona 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 Tarefas, tanto PackageReference quanto ProjectReference com o atributo PrivateAssets="all". Isto irá indicar ao NuGet para não expor essas dependências a nenhum dos projetos consumidores. 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 incorporar os ativos em tempo de execução das nossas dependências no pacote de tarefas. Há duas partes para isso: um alvo MSBuild que adiciona as nossas dependências ao BuildOutputInPackage ItemGroup e algumas propriedades que controlam o layout desses BuildOutputInPackage itens. 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 inclua o assembly Microsoft.Build.Utilities.Core

Como discutido acima, essa dependência será fornecida pelo próprio MSBuild em tempo de execução, 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 incorporar 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, já que 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 num pacote

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

Em alguns casos, talvez você queira adicionar destinos ou propriedades de compilação personalizados em projetos que consomem seu pacote, como a execução de uma ferramenta ou processo personalizado durante a compilação. Para fazer isso, coloque os arquivos no formato <package_id>.targets ou <package_id>.props dentro da pasta build no projeto.

Os arquivos na pasta raiz compilação do projeto 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 a partir 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 de compilação e, nessa pasta, adicione dois ficheiros de texto: AppSettingStronglyTyped.props e AppSettingStronglyTyped.targets. AppSettingStronglyTyped.props é importado no início do Microsoft.Common.props, e as propriedades que são definidas posteriormente não estão disponíveis para ele. Por isso, evite referir-se a propriedades que ainda não estão definidas; elas seriam avaliadas como vazias.

    Directory.Build.targets é importado de Microsoft.Common.targets depois de importar arquivos .targets de pacotes NuGet. Assim, pode substituir propriedades e alvos definidos na maior parte da lógica de construção ou definir propriedades para todos os seus projetos, independentemente do que os projetos individuais definam. Consulte a 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>
    
  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, nunca é usado. Para colocar esse código em ação, defina alguns destinos no arquivo AppSettingStronglyTyped.targets, que também serão incluídos 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>
    

    O primeiro passo é a criação de um ItemGroup, que representa os arquivos de texto (pode ser mais de um) para ler e será algum do nosso parâmetro de tarefa. Há valores padrão para o local e a extensão onde procuramos, mas você pode substituir os valores que definem as propriedades no arquivo de projeto MSBuild cliente.

    Em seguida, defina dois targets MSBuild. Nós estendemos o processo MSBuild, substituindo os targets predefinidos:

    • BeforeCompile: O objetivo é chamar a tarefa personalizada para gerar a classe e incluir a classe a ser compilada. As tarefas neste destino são inseridas antes que a compilação principal seja concluída. Os campos de Entrada e Saída estão relacionados à compilação incremental. Se todos os itens de saída estiverem com a propriedade up-to-data, o MSBuild irá ignorar o alvo. Essa compilação incremental do destino pode melhorar significativamente o desempenho de suas compilações. Um item é considerado up-to-date se seu arquivo de saída tiver a mesma idade ou for mais recente do que seu arquivo ou arquivos de entrada.

    • AfterClean: O objetivo é excluir o arquivo de classe gerado depois que uma limpeza geral acontece. As tarefas neste destino são inseridas depois que a funcionalidade de limpeza principal é invocada. Isso força a etapa de geração de código a ser repetida quando o alvo Rebuild é 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 Gerenciador de Soluções e selecione Pack). Você também pode fazer isso usando a linha de comando. Navegue até a pasta onde 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 multialvo

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

Para projetos SDK .NET 'normais', multitargeting significa definir vários TargetFrameworks em seu arquivo de projeto. Quando você fizer isso, as compilações serão acionadas para ambos os TargetFrameworkMonikers, e os resultados gerais podem ser empacotados como um único artefato.

Essa não é a história completa do MSBuild. O MSBuild tem dois veículos de remessa principais: Visual Studio e o SDK do .NET. Estes são ambientes de tempo de execução muito diferentes; um é executado no tempo de execução do .NET Framework e outro é executado no CoreCLR. O que isso significa é que, embora seu código possa ter como destino netstandard2.0, sua lógica de tarefa pode ter diferenças com base no tipo de tempo de execução do MSBuild está em uso no momento. Praticamente, como há tantas APIs novas no .NET 5.0 e superiores, faz sentido tanto multidirecionar o código-fonte da tarefa do MSBuild para vários TargetFrameworkMonikers, quanto multidirecionar a lógica de destino do MSBuild para vários tipos de tempo de execução do MSBuild.

Alterações necessárias para multitarget

Para direcionar múltiplos TargetFrameworkMonikers (TFM):

  1. Altere o seu ficheiro de projeto para usar os TFMs net472 e net6.0 (este último pode mudar com base no nível do SDK que pretende destinar). Talvez queiras apontar para netcoreapp3.1 até que o .NET Core 3.1 deixe de receber suporte. Quando você faz isso, a estrutura de pastas do pacote muda de tasks/ para tasks/<TFM>/.

    <TargetFrameworks>net472;net6.0</TargetFrameworks>
    
  2. Atualize os seus arquivos .targets para utilizar o TFM correto a fim de carregar as suas tarefas. O TFM necessário será alterado com base no TFM do .NET que você escolheu acima, mas para um projeto direcionado a 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 estiver definida, você poderá usá-la no UsingTask para carregar o AssemblyFilecorreto:

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

Próximos passos

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 percorre ambas as opções com um cenário de geração de código mais realista: criando uma tarefa personalizada para gerar código de cliente para uma API REST.

Ou saiba como testar uma tarefa personalizada.