Compartir a través de


Crear una tarea inline de MSBuild con la RoslynCodeTaskFactory

RoslynCodeTaskFactory usa los compiladores Roslyn multiplataforma para generar ensamblajes de tarea en memoria para su uso como tareas integradas. RoslynCodeTaskFactory las tareas tienen como destino .NET Standard y pueden funcionar en entornos de ejecución de .NET Framework y .NET Core, así como en otras plataformas, como Linux y macOS.

Nota:

RoslynCodeTaskFactory solo está disponible en MSBuild 15.8 y versiones posteriores. Las versiones de MSBuild siguen las versiones de Visual Studio, por lo que RoslynCodeTaskFactory está disponible en Visual Studio 2017, versión 15.8 y posteriores.

Estructura de una tarea en línea con RoslynCodeTaskFactory

RoslynCodeTaskFactory Las tareas en línea se declaran mediante el elemento UsingTask. La tarea insertada junto con el elemento UsingTask que la contiene normalmente se incluye en un archivo .targets y se importa en otros archivos de proyecto según sea necesario. Esta es una tarea básica en línea. Observe que no hace 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>

El UsingTask elemento del ejemplo tiene tres atributos que describen la tarea y el generador de tareas insertados que lo compila.

  • El TaskName atributo asigna un nombre a la tarea, en este caso, DoNothing.

  • El TaskFactory atributo asigna un nombre a la clase que implementa el generador de tareas insertado.

  • El AssemblyFile atributo proporciona la ubicación del generador de tareas insertado. Como alternativa, puede usar el AssemblyName atributo para especificar el nombre completo de la clase del generador de tareas en línea, que normalmente se encuentra en la caché global de ensamblados (GAC).

Los elementos restantes de la tarea DoNothing están vacíos y se proporcionan para ilustrar el orden y la estructura de una tarea en línea. En este artículo se presenta un ejemplo más sólido.

  • El elemento ParameterGroup es opcional. Cuando se especifica, declara los parámetros de la tarea. Para obtener más información sobre los parámetros de entrada y salida, consulte Parámetros de entrada y salida más adelante en este artículo.

  • El Task elemento describe y contiene el código fuente de la tarea.

  • El Reference elemento especifica referencias a los ensamblados de .NET que usa en el código. Esto equivale a agregar una referencia a un proyecto en Visual Studio. El Include atributo especifica la ruta de acceso del ensamblado al que se hace referencia.

  • El Using elemento enumera los espacios de nombres a los que desea acceder. Este elemento es similar a la using directiva de Visual C#. El Namespace atributo especifica el espacio de nombres que se va a incluir.

Reference y Using son elementos independientes del lenguaje. Las tareas en línea se pueden escribir en cualquiera de los lenguajes .NET CodeDom admitidos, por ejemplo, Visual Basic o Visual C#.

Nota:

Los elementos contenidos por el Task elemento son específicos del generador de tareas, en este caso, el generador de tareas de código.

Elemento de código

El último elemento secundario que se va a aparecer dentro del Task elemento es el Code elemento . El Code elemento contiene o busca el código que desea compilar en una tarea. Lo que se coloca en el Code elemento depende de cómo quiera escribir la tarea.

El Language atributo especifica el idioma en el que se escribe el código. Los valores aceptables son cs para C#, vb para Visual Basic.

El Type atributo especifica el tipo de código que se encuentra en el Code elemento .

  • Si el valor de Type es Class, el Code elemento contiene código para una clase que deriva de la ITask interfaz .

  • Si el valor de Type es Method, el código define una sobrescritura del método Execute de la interfaz ITask.

  • Si el valor de Type es Fragment, el código define el contenido del Execute método, pero no la firma ni la return instrucción .

El propio código suele aparecer entre un <![CDATA[ marcador y un ]]> marcador. Dado que el código está en una sección CDATA, no tiene que preocuparse por el escape de caracteres reservados, por ejemplo, "<" o ">".

Como alternativa, puede usar el Source atributo del Code elemento para especificar la ubicación de un archivo que contiene el código de la tarea. El código del archivo de origen debe ser del tipo especificado por el Type atributo . Si el Source atributo está presente, el valor predeterminado de Type es Class. Si Source no está presente, el valor predeterminado es Fragment.

Nota:

Si define la clase de tarea en un archivo de origen, el nombre de clase debe estar de acuerdo con el TaskName atributo del elemento UsingTask correspondiente.

Hola mundo

Esta es una tarea en línea más sólida con RoslynCodeTaskFactory. La tarea HelloWorld muestra "Hello, world!" en el dispositivo de registro de errores predeterminado, que suele ser la consola del sistema o la ventana Salida de Visual Studio. El Reference elemento del ejemplo se incluye solo para la ilustración.

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

Puede guardar la HelloWorld tarea en un archivo denominado HelloWorld.targets y, a continuación, invocarla desde un proyecto como se indica a continuación.

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

Parámetros de entrada y salida

Los parámetros de tarea insertados son elementos secundarios de un elemento ParameterGroup. Cada parámetro toma el nombre del elemento que lo define. El código siguiente define el parámetro Text.

<ParameterGroup>
    <Text />
</ParameterGroup>

Los parámetros pueden tener uno o varios de estos atributos:

  • Required es un atributo opcional que es false de forma predeterminada. Si true está presente, el parámetro es necesario y debe tener un valor antes de ejecutar la tarea.

  • ParameterType es un atributo opcional que es System.String de forma predeterminada. Puede configurarse en cualquier tipo completamente calificado que sea un elemento o un valor que se pueda convertir en una cadena mediante System.Convert.ChangeType. (Es decir, cualquier tipo que se pueda pasar a una tarea externa y de vuelta).

  • Output es un atributo opcional que es false de forma predeterminada. Si true, el parámetro debe tener un valor antes de regresar del método Execute.

Por ejemplo

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

define estos tres parámetros:

  • Expression es un parámetro de entrada necesario de tipo System.String.

  • Files es un parámetro de entrada de lista de elementos obligatorio.

  • Tally es un parámetro de salida de tipo System.Int32.

Si el Code elemento tiene el Type atributo de Fragment o Method, las propiedades se crean automáticamente para cada parámetro. En RoslynCodeTaskFactory, si el Code elemento tiene el Type atributo de Class, no tiene que especificar el ParameterGroup, ya que se deduce del código fuente (esto es una diferencia con respecto a CodeTaskFactory). De lo contrario, las propiedades deben declararse explícitamente en el código fuente de la tarea y deben coincidir exactamente con sus definiciones de parámetros.

Ejemplo

La siguiente tarea en línea registra algunos mensajes y devuelve una cadena.

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

Estas tareas en línea pueden combinar rutas y obtener el nombre del archivo.

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

Proporcionar compatibilidad con versiones anteriores

RoslynCodeTaskFactory primero se puso a disposición en la versión 15.8 de MSBuild. Supongamos que quiere admitir versiones anteriores de Visual Studio y MSBuild, cuando RoslynCodeTaskFactory no estaba disponible, pero CodeTaskFactory era, pero quiere usar el mismo script de compilación. Puede usar un Choose constructo que utiliza la $(MSBuildVersion) propiedad para decidir en tiempo de compilación si usar RoslynCodeTaskFactory o volver a CodeTaskFactory, como en el siguiente ejemplo:

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