Partilhar via


Criar uma tarefa embutida do MSBuild com RoslynCodeTaskFactory

RoslynCodeTaskFactory usa os compiladores Roslyn de plataforma cruzada para gerar assemblies de tarefas na memória para uso como tarefas embutidas. RoslynCodeTaskFactory as tarefas destinam-se ao .NET Standard e podem funcionar em tempos de execução 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 UsingTask elemento que a 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 básica em linha. Note que 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 embutida que a compila.

  • O TaskName atributo nomeia a tarefa, neste 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 embutidas, que normalmente está localizada no cache de assembly global (GAC).

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 mais adiante neste artigo.

  • O ParameterGroup elemento é 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 é equivalente 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. Este elemento é semelhante à using diretiva no Visual C#. O Namespace atributo especifica o namespace a ser incluído.

Reference e Using os elementos são agnósticos em relação à linguagem. As tarefas embutidas podem ser escritas em qualquer uma das linguagens .NET CodeDom suportadas, 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, neste caso, a fábrica de tarefas de código.

Elemento de código

O último elemento filho a aparecer 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 de Type é Class, então o Code elemento contém código para uma classe que deriva da ITask interface.

  • Se o valor de Type é Method, então o código define uma substituição do Execute método da ITask interface.

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

O código em si 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 em escapar de caracteres reservados, por exemplo, "<" ou ">".

Como alternativa, você pode usar o Source atributo do elemento para especificar o Code 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 padrão de Type é 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.

Hello World

Aqui está uma tarefa em linha mais robusta com RoslynCodeTaskFactoryo . A tarefa HelloWorld exibe "Olá, mundo!" no dispositivo de log de erros padrão, que normalmente é o console do sistema ou a janela de 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, em seguida, invocá-la a partir de um projeto da seguinte maneira.

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

Parâmetros de entrada e saída

Os parâmetros de tarefa embutidos são elementos filho de um ParameterGroup elemento. Cada parâmetro leva 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 destes atributos:

  • Required é um atributo opcional que é false por padrão. Se true, então 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 em e de 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, então o parâmetro deve 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 obrigatório.

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

Se o Code elemento tiver o Type atributo de Fragment ou Method, as propriedades serão criadas automaticamente para cada parâmetro. Em RoslynCodeTaskFactory, se o Code elemento tiver o Type atributo de Class, então você não precisa especificar o ParameterGroup, uma vez que ele é inferido a partir do código-fonte (esta é uma diferença de CodeTaskFactory). Caso contrário, as propriedades devem ser explicitamente declaradas no código-fonte da tarefa e devem corresponder exatamente às suas 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 tornou-se disponível no MSBuild versão 15.8. Suponha que você queira oferecer suporte a versões anteriores do Visual Studio e MSBuild, quando RoslynCodeTaskFactory não estava disponível, mas CodeTaskFactory estava, mas deseja usar o mesmo script de compilação. Você pode usar uma Choose construção que usa a $(MSBuildVersion) propriedade para decidir em tempo de compilação se deve usar o RoslynCodeTaskFactory ou fallback 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>