Compartilhar via


Criar uma tarefa embutida do MSBuild com RoslynCodeTaskFactory

RoslynCodeTaskFactory usa os compiladores Roslyn multiplataforma para gerar assemblies de tarefas na memória para uso como tarefas embutidas. RoslynCodeTaskFactory as tarefas são direcionadas ao .NET Standard e podem funcionar em runtimes do .NET Framework e do .NET Core, bem como em outras plataformas, como Linux e macOS.

Observação

O RoslynCodeTaskFactory está disponível apenas no MSBuild 15.8 e superior. As versões do MSBuild seguem as versões do Visual Studio, portanto, RoslynCodeTaskFactory está disponível no Visual Studio 2017 versão 15.8 e superior.

A estrutura de uma tarefa embutida com RoslynCodeTaskFactory

RoslynCodeTaskFactory As tarefas embutidas são declaradas usando o UsingTask elemento. A tarefa embutida e o elemento que a UsingTask contém normalmente são incluídos em um .targets arquivo e importados para outros arquivos de projeto, conforme necessário. Aqui está uma tarefa embutida básica. Observe que ele não faz nada.

<Project>
  <!-- This simple inline task does nothing. -->
  <UsingTask
    TaskName="DoNothing"
    TaskFactory="RoslynCodeTaskFactory"
    AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll" >
    <ParameterGroup />
    <Task>
      <Reference Include="" />
      <Using Namespace="" />
      <Code Type="Fragment" Language="cs">
      </Code>
    </Task>
  </UsingTask>
</Project>

O UsingTask elemento no exemplo tem três atributos que descrevem a tarefa e a fábrica de tarefas embutidas que a compila.

  • O TaskName atributo nomeia a tarefa, nesse caso, DoNothing.

  • O TaskFactory atributo nomeia a classe que implementa a fábrica de tarefas embutidas.

  • O AssemblyFile atributo fornece o local da fábrica de tarefas embutidas. Como alternativa, você pode usar o AssemblyName atributo para especificar o nome totalmente qualificado da classe de fábrica de tarefas embutida, que normalmente está localizada no GAC (cache de assembly global).

Os elementos restantes da DoNothing tarefa estão vazios e são fornecidos para ilustrar a ordem e a estrutura de uma tarefa embutida. Um exemplo mais robusto é apresentado posteriormente neste artigo.

  • O elemento ParameterGroup é opcional. Quando especificado, ele declara os parâmetros para a tarefa. Para obter mais informações sobre parâmetros de entrada e saída, consulte Parâmetros de entrada e saída mais adiante neste artigo.

  • O Task elemento descreve e contém o código-fonte da tarefa.

  • O Reference elemento especifica referências aos assemblies .NET que você está usando em seu código. Isso equivale a adicionar uma referência a um projeto no Visual Studio. O Include atributo especifica o caminho do assembly referenciado.

  • O Using elemento lista os namespaces que você deseja acessar. Esse elemento se assemelha à using diretiva no Visual C#. O Namespace atributo especifica o namespace a ser incluído.

Reference e Using os elementos são independentes de linguagem. As tarefas embutidas podem ser escritas em qualquer uma das linguagens de CodeDom do .NET com suporte, por exemplo, Visual Basic ou Visual C#.

Observação

Os elementos contidos pelo Task elemento são específicos para a fábrica de tarefas, nesse caso, a fábrica de tarefas de código.

Elemento code

O último elemento filho a ser exibido dentro do Task elemento é o Code elemento. O Code elemento contém ou localiza o código que você deseja que seja compilado em uma tarefa. O que você coloca no Code elemento depende de como você deseja escrever a tarefa.

O Language atributo especifica o idioma no qual o código é escrito. Os valores aceitáveis são cs para C#, vb para Visual Basic.

O Type atributo especifica o tipo de código encontrado no Code elemento.

  • Se o valor Type for Class, o elemento conterá código Code para uma classe derivada da ITask interface.

  • Se o valor Type for Method, o código definirá uma substituição do Execute método da ITask interface.

  • Se o valor Type for Fragment, o código definirá o conteúdo do Execute método, mas não a assinatura ou a instrução return .

O próprio código normalmente aparece entre um <![CDATA[ marcador e um ]]> marcador. Como o código está em uma seção CDATA, você não precisa se preocupar com o escape de caracteres reservados, por exemplo, "<" ou ">".

Como alternativa, você pode usar o Source atributo do Code elemento para especificar o local de um arquivo que contém o código para sua tarefa. O código no arquivo de origem deve ser do tipo especificado pelo Type atributo. Se o Source atributo estiver presente, o valor Type padrão será Class. Se Source não estiver presente, o valor padrão será Fragment.

Observação

Se você definir a classe de tarefa em um arquivo de origem, o nome da classe deverá concordar com o TaskName atributo do elemento UsingTask correspondente.

Olá, Mundo

Aqui está uma tarefa embutida mais robusta com RoslynCodeTaskFactory. A tarefa HelloWorld exibe "Olá, mundo!" no dispositivo de log de erros padrão, que normalmente é o console do sistema ou a janela Saída do Visual Studio. O Reference elemento no exemplo é incluído apenas para ilustração.

<Project>
  <!-- This simple inline task displays "Hello, world!" -->
  <UsingTask
    TaskName="HelloWorld"
    TaskFactory="RoslynCodeTaskFactory"
    AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll" >
    <ParameterGroup />
    <Task>
      <Reference Include="System.Xml"/>
      <Using Namespace="System"/>
      <Using Namespace="System.IO"/>
      <Code Type="Fragment" Language="cs">
<![CDATA[
// Display "Hello, world!"
Log.LogError("Hello, world!");
]]>
      </Code>
    </Task>
  </UsingTask>
</Project>

Você pode salvar a HelloWorld tarefa em um arquivo chamado HelloWorld.targets e invocá-la de um projeto da seguinte maneira.

<Project>
  <Import Project="HelloWorld.targets" />
  <Target Name="Hello">
    <HelloWorld />
  </Target>
</Project>

Parâmetros de entrada e saída

Parâmetros de tarefa embutidos são elementos filho de um ParameterGroup elemento. Cada parâmetro usa o nome do elemento que o define. O código a seguir define o parâmetro Text.

<ParameterGroup>
    <Text />
</ParameterGroup>

Os parâmetros podem ter um ou mais desses atributos:

  • Required é um atributo opcional que é false por padrão. Se true, o parâmetro é necessário e deve receber um valor antes de chamar a tarefa.

  • ParameterType é um atributo opcional que é System.String por padrão. Ele pode ser definido como qualquer tipo totalmente qualificado que seja um item ou um valor que possa ser convertido de e para uma cadeia de caracteres usando System.Convert.ChangeType. (Em outras palavras, qualquer tipo que possa ser passado de e para uma tarefa externa.)

  • Output é um atributo opcional que é false por padrão. Se true, o parâmetro deverá receber um valor antes de retornar do método Execute.

Por exemplo

<ParameterGroup>
    <Expression Required="true" />
    <Files ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" />
    <Tally ParameterType="System.Int32" Output="true" />
</ParameterGroup>

define estes três parâmetros:

  • Expression é um parâmetro de entrada necessário do tipo System.String.

  • Files é um parâmetro de entrada de lista de itens necessário.

  • Tally é um parâmetro de saída do tipo System.Int32.

Se o Code elemento tiver o Type atributo de Fragment ou Method, em seguida, as propriedades serão criadas automaticamente para cada parâmetro. Em RoslynCodeTaskFactory, se o Code elemento tem o Type atributo de Class, então você não precisa especificar o ParameterGroup, pois ele é inferido do código-fonte (essa é uma diferença de CodeTaskFactory). Caso contrário, as propriedades devem ser declaradas explicitamente no código-fonte da tarefa e devem corresponder exatamente às definições de parâmetro.

Exemplo

A tarefa embutida a seguir registra algumas mensagens e retorna uma cadeia de caracteres.

<Project>

    <UsingTask TaskName="MySample"
               TaskFactory="RoslynCodeTaskFactory"
               AssemblyFile="$(MSBuildBinPath)\Microsoft.Build.Tasks.Core.dll">
        <ParameterGroup>
            <Parameter1 ParameterType="System.String" Required="true" />
            <Parameter2 ParameterType="System.String" />
            <Parameter3 ParameterType="System.String" Output="true" />
        </ParameterGroup>
        <Task>
            <Using Namespace="System" />
            <Code Type="Fragment" Language="cs">
              <![CDATA[
              Log.LogMessage(MessageImportance.High, "Hello from an inline task created by Roslyn!");
              Log.LogMessageFromText($"Parameter1: '{Parameter1}'", MessageImportance.High);
              Log.LogMessageFromText($"Parameter2: '{Parameter2}'", MessageImportance.High);
              Parameter3 = "A value from the Roslyn CodeTaskFactory";
            ]]>
            </Code>
        </Task>
    </UsingTask>

    <Target Name="Demo">
      <MySample Parameter1="A value for parameter 1" Parameter2="A value for parameter 2">
          <Output TaskParameter="Parameter3" PropertyName="NewProperty" />
      </MySample>

      <Message Text="NewProperty: '$(NewProperty)'" />
    </Target>
</Project>

Essas tarefas embutidas podem combinar caminhos e obter o nome do arquivo.

<Project>

    <UsingTask TaskName="PathCombine"
               TaskFactory="RoslynCodeTaskFactory"
               AssemblyFile="$(MSBuildBinPath)\Microsoft.Build.Tasks.Core.dll">
        <ParameterGroup>
            <Paths ParameterType="System.String[]" Required="true" />
            <Combined ParameterType="System.String" Output="true" />
        </ParameterGroup>
        <Task>
            <Using Namespace="System" />
            <Code Type="Fragment" Language="cs">
            <![CDATA[
            Combined = Path.Combine(Paths);
            ]]>
            </Code>
        </Task>
    </UsingTask>

    <UsingTask TaskName="PathGetFileName"
             TaskFactory="RoslynCodeTaskFactory"
             AssemblyFile="$(MSBuildBinPath)\Microsoft.Build.Tasks.Core.dll">
        <ParameterGroup>
            <Path ParameterType="System.String" Required="true" />
            <FileName ParameterType="System.String" Output="true" />
        </ParameterGroup>
        <Task>
            <Using Namespace="System" />
            <Code Type="Fragment" Language="cs">
            <![CDATA[
            FileName = System.IO.Path.GetFileName(Path);
            ]]>
            </Code>
        </Task>
    </UsingTask>

    <Target Name="Demo">
        <PathCombine Paths="$(Temp);MyFolder;$([System.Guid]::NewGuid()).txt">
            <Output TaskParameter="Combined" PropertyName="MyCombinedPaths" />
        </PathCombine>

        <Message Text="Combined Paths: '$(MyCombinedPaths)'" />

        <PathGetFileName Path="$(MyCombinedPaths)">
            <Output TaskParameter="FileName" PropertyName="MyFileName" />
        </PathGetFileName>

        <Message Text="File name: '$(MyFileName)'" />
    </Target>
</Project>

Fornecer compatibilidade com versões anteriores

RoslynCodeTaskFactory primeiro ficou disponível no MSBuild versão 15.8. Suponha que você queira dar suporte a versões anteriores do Visual Studio e do MSBuild, quando RoslynCodeTaskFactory não estava disponível, mas CodeTaskFactory estava, mas deseja usar o mesmo script de build. Você pode usar um Choose constructo que usa a $(MSBuildVersion) propriedade para decidir no momento da compilação se deseja usar ou RoslynCodeTaskFactory recuar para CodeTaskFactory, como no exemplo a seguir:

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

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>

  <Choose>
    <When Condition=" '$(MSBuildVersion.Substring(0,2))' >= 16 Or
    ('$(MSBuildVersion.Substring(0,2))' == 15 And '$(MSBuildVersion.Substring(3,1))' >= 8)">
      <PropertyGroup>
        <TaskFactory>RoslynCodeTaskFactory</TaskFactory>
      </PropertyGroup>
    </When>
    <Otherwise>
      <PropertyGroup>
        <TaskFactory>CodeTaskFactory</TaskFactory>
      </PropertyGroup>
    </Otherwise>
  </Choose>
  
  <UsingTask
    TaskName="HelloWorld"
    TaskFactory="$(TaskFactory)"
    AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
    <ParameterGroup />
    <Task>
      <Using Namespace="System"/>
      <Using Namespace="System.IO"/>
      <Code Type="Fragment" Language="cs">
        <![CDATA[
         Log.LogError("Using RoslynCodeTaskFactory");
      ]]>
      </Code>
    </Task>
  </UsingTask>

  <Target Name="RunTask" AfterTargets="Build">
    <Message Text="MSBuildVersion: $(MSBuildVersion)"/>
    <Message Text="TaskFactory: $(TaskFactory)"/>
    <HelloWorld />
  </Target>

</Project>
  • Tarefas