MSBuild 内联任务

MSBuild 任务通常通过编译实现接口的 ITask 类来创建。 有关详细信息,请参阅 任务

如果想要避免创建经过编译的任务的开销,可以在项目文件或导入文件中内嵌创建任务。 无需创建单独的程序集来托管任务。 使用内联任务可以更轻松地跟踪源代码并更轻松地部署任务。 源代码集成到 MSBuild 项目文件或导入的文件中,通常是一个 .targets 文件。

使用 代码任务工厂创建内联任务。 对于当前开发,请务必使用 RoslynCodeTaskFactory,而不是 CodeTaskFactoryCodeTaskFactory 仅支持最多 4.0 的 C# 版本。

内联任务是为了方便处理不需要复杂依赖项的小型任务而设计的。 对内联任务的调试支持有限。 建议在想要编写更复杂的代码、引用 NuGet 包、运行外部工具或执行可能生成错误条件的作时创建已编译的任务,而不是内联任务。 此外,每次生成时都会编译内联任务,因此可能会对生成性能产生明显影响。

内联任务的结构

内联任务由 UsingTask 元素包含。 内联任务及包含它的 UsingTask 元素通常被包含在 .targets 文件中,并根据需要导入到其他项目文件。 下面是一个不执行任何操作的基本的内联任务,但展示了语法:

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

示例中 UsingTask 的元素有三个属性,用于描述任务和编译任务的内联任务工厂。

  • 属性 TaskName 将任务命名,在本例中为 DoNothing该任务命名。

  • 属性 TaskFactory 命名了实现内联任务工厂的类。

  • AssemblyFile 属性提供内联任务工厂的位置。 或者,可以使用 AssemblyName 属性指定内联任务工厂类的完全限定名称,该类通常位于内联任务工厂类中 $(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll

任务的其余元素 DoNothing 是空的,用于说明内联任务的顺序和结构。 本文稍后将介绍完整的示例。

  • ParameterGroup 元素是可选的。 指定后,它会声明任务的参数。 有关输入和输出参数的详细信息,请参阅本文后面的 输入和输出参数

  • Task 元素描述并包含任务源代码。

  • Reference 元素指定对代码中正在使用的 .NET 程序集的引用。 使用此元素等效于在 Visual Studio 中添加对项目的引用。 该 Include 特性指定所引用程序集的路径。 mscorlib、.NET Standard、Microsoft.Build.FrameworkMicrosoft.Build.Utilities.Core 中的程序集,以及一些传递引用为依赖项的程序集,均可在没有 Reference 的情况下使用。

  • Using 元素列出要访问的命名空间。 此元素等效于 using C# 中的指令。 该 Namespace 属性指定要包含的命名空间。 在内联代码中放入using指令是无效的,因为该代码被放入方法体中,而在方法体中不允许使用using指令。

ReferenceUsing 元素与语言无关。 内联任务可以用 Visual Basic 或 C# 编写。

注释

Task 元素所包含的元素是特定于任务工厂的,在本例中是代码任务工厂。

Code 元素

Task 元素中,最后出现的子元素是 Code 元素。 该 Code 元素包含或查找要编译到任务中的代码。 在 Code 元素中放置的内容取决于您如何编写任务。

Language 属性指定编写代码的语言。 可接受的值为 cs C#, vb 对于 Visual Basic。

Type 特性指定元素 Code 中找到的代码类型。

  • 如果Type的值为Class,则Code元素包含派生自ITask接口的类的代码。

  • 如果Type的值为Method,则代码定义了ITask接口的Execute方法的重写。

  • 如果Type的值是Fragment,则代码定义Execute方法的内容,而不是签名或return语句。

代码本身通常出现在<![CDATA[标记和]]>标记之间。 由于代码位于 CDATA 部分,因此无需担心转义保留字符,例如 < 或 >。

或者,可以使用 Source 元素的属性 Code 来指定包含任务代码的文件的位置。 源文件中的代码必须是由 Type 特性指定的类型。 Source如果该属性存在,则默认值为 TypeClass. 如果 Source 不存在,则默认值为 Fragment

注释

在源文件中定义任务类时,类名必须与相应的 TaskName 元素的属性一致

HelloWorld

下面是一个简单的内联任务示例。 HelloWorld 任务在默认错误日志记录设备上显示“Hello, world!” ,通常是系统控制台或 Visual Studio 输出 窗口。

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

可以将 HelloWorld 任务保存在名为 HelloWorld.targets 的文件中,然后从项目调用它,如下所示。

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

输入和输出参数

内联任务参数是 ParameterGroup 元素的子元素。 每个参数都采用定义它的元素的名称。 以下代码定义参数 Text

<ParameterGroup>
  <Text />
</ParameterGroup>

参数可能具有以下一个或多个属性:

  • Required 是默认的 false 可选属性。 如果是 true,则参数是必需的,并且必须在调用任务之前提供一个值。
  • ParameterType 是默认的 System.String 可选属性。 它可以设置为任何完全限定的类型,该类型要么是项,要么是可以通过 ChangeType 转换为字符串或从字符串转换的值。 (换句话说,可以传入和传出外部任务的任何类型。
  • Output 是默认的 false 可选属性。 如果 true,则必须在从 Execute 方法返回之前为参数提供一个值。

例如,

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

定义以下三个参数:

  • Expression 是 System.String 类型的必需输入参数。

  • Files 是必需的项列表输入参数。

  • Tally 是 System.Int32 类型的输出参数。

如果Code元素具有Type属性为FragmentMethod,则会自动为每个参数创建属性。 否则,必须在任务源代码中显式声明属性,并且必须完全匹配其参数定义。

调试内联任务

MSBuild 生成内联任务的源文件,并将输出以 GUID 文件名的形式写入位于临时文件夹 AppData\Local\Temp\MSBuildTemp 中的文本文件。 输出通常被删除,但要保留此输出文件,可以将环境变量 MSBUILDLOGCODETASKFACTORYOUTPUT 设置为 1。

示例 1

以下内联任务将指定文件中所有出现的令牌替换为指定的值。

<Project>

  <UsingTask TaskName="TokenReplace" TaskFactory="RoslynCodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
    <ParameterGroup>
      <Path ParameterType="System.String" Required="true" />
      <Token ParameterType="System.String" Required="true" />
      <Replacement ParameterType="System.String" Required="true" />
    </ParameterGroup>
    <Task>
      <Code Type="Fragment" Language="cs"><![CDATA[
string content = File.ReadAllText(Path);
content = content.Replace(Token, Replacement);
File.WriteAllText(Path, content);

]]></Code>
    </Task>
  </UsingTask>

  <Target Name='Demo' >
    <TokenReplace Path="Target.config" Token="$MyToken$" Replacement="MyValue"/>
  </Target>
</Project>

示例 2

以下内联任务生成序列化输出。 此示例演示如何使用输出参数和引用。

<Project>
  <PropertyGroup>
    <RoslynCodeTaskFactoryAssembly Condition="$(RoslynCodeTaskFactoryAssembly) == ''">$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll</RoslynCodeTaskFactoryAssembly>
  </PropertyGroup>

    <UsingTask 
    TaskName="MyInlineTask" 
    TaskFactory="RoslynCodeTaskFactory" 
    AssemblyFile="$(RoslynCodeTaskFactoryAssembly)">
    <ParameterGroup>
      <Input ParameterType="System.String" Required="true" />
      <Output ParameterType="System.String" Output="true" />
    </ParameterGroup>
    <Task>
      <Reference Include="System.Text.Json" /> <!-- Reference an assembly -->
      <Using Namespace="System.Text.Json" />   <!-- Use a namespace -->
      <Code Type="Fragment" Language="cs">
        <![CDATA[
          Output = JsonSerializer.Serialize(new { Message = Input });
        ]]>
      </Code>
    </Task>
  </UsingTask>

  <Target Name="RunInlineTask">
    <MyInlineTask Input="Hello, Roslyn!" >
      <Output TaskParameter="Output" PropertyName="SerializedOutput" />
    </MyInlineTask>
    <Message Text="Serialized Output: $(SerializedOutput)" />
  </Target>
</Project>