教程:创建自定义任务以生成代码
在本教程中,你将在 C# 中的 MSBuild 中创建一个用于处理代码生成的自定义任务,然后在生成中使用该任务。 此示例演示如何使用 MSBuild 来处理清理和重新生成操作。 该示例还演示如何支持增量生成,以便仅在输入文件发生更改时生成代码。 演示的技术适用于各种代码生成方案。 这些步骤还演示了如何使用 NuGet 打包分发任务,本教程包含一个可选步骤,用于使用 BinLog 查看器来改善故障排除体验。
先决条件
你应该了解 MSBuild 概念,例如任务、目标和属性。 请参阅 MSBuild 概念。
这些示例需要使用 Visual Studio 安装的 MSBuild,但也可以单独安装。 请参阅下载 MSBuild 而不下载 Visual Studio。
代码示例简介
该示例采用包含要设置的值的输入文本文件,并使用创建这些值的代码创建一个 C# 代码文件。 虽然这是一个简单的示例,但相同的基本技术可以应用于更复杂的代码生成方案。
在本教程中,你将创建一个名为 AppSettingStronglyTyped 的 MSBuild 自定义任务。 该任务将读取一组文本文件,其中每个文件包含具有以下格式的行:
propertyName:type:defaultValue
该代码生成一个 C# 类,其中包含所有常量。 问题应停止生成,并为用户提供足够的信息来诊断问题。
本教程的完整示例代码位于 GitHub 上的 .NET 示例仓库中的 自定义任务 - 代码生成。
创建 AppSettingStronglyTyped 项目
创建 .NET Standard 类库。 框架应为 .NET Standard 2.0。
请注意完整 MSBuild(Visual Studio 使用的 MSBuild)和可移植 MSBuild(捆绑在 .NET Core 命令行中的 MSBuild)之间的差异。
- 完整的 MSBuild:此版本的 MSBuild 通常位于 Visual Studio 中。 在 .NET Framework 上运行。 在解决方案或项目上执行“生成”时,Visual Studio 会使用此 MSBuild。 此版本也可在命令行环境中使用,例如 Visual Studio 开发人员命令提示符或 PowerShell。
- .NET MSBuild:此版本的 MSBuild 捆绑在 .NET Core 命令行中。 它在 .NET Core 上运行。 Visual Studio 不直接调用此版本的 MSBuild。 它仅支持使用 Microsoft.NET.Sdk 生成的项目。
如果要在 .NET Framework 和任何其他 .NET 实现(如 .NET Core)之间共享代码,则库应面向 .NET Standard 2.0,并且想要在 .NET Framework 上运行的 Visual Studio 中运行。 .NET Framework 不支持 .NET Standard 2.1。
选择要引用的 MSBuild API 版本
编译自定义任务时,应引用与所需的 Visual Studio 和/或 .NET SDK 的最低版本匹配的 MSBuild API(Microsoft.Build.*
)版本。 例如,若要支持 Visual Studio 2019 上的用户,应根据 MSBuild 16.11 进行生成。
创建 AppSettingStronglyTyped MSBuild 自定义任务
第一步是创建 MSBuild 自定义任务。 有关如何编写 MSBuild 自定义任务的信息可帮助你了解以下步骤。 MSBuild 自定义任务是实现 ITask 接口的类。
添加对 Microsoft.Build.Utilities.Core NuGet 包的引用,然后创建派生自 Microsoft.Build.Utilities.Task 的名为 AppSettingStronglyType 的类。
添加三个属性。 这些属性定义用户在客户端项目中使用任务时设置的任务的参数:
//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; }
该任务处理 SettingFiles 并生成类
SettingNamespaceName.SettingClassName
。 生成的类将基于文本文件的内容具有一组常量。任务输出应是一个字符串,该字符串提供生成的代码的文件名:
// The filename where the class was generated [Output] public string ClassNameFile { get; set; }
创建自定义任务时,将从 Microsoft.Build.Utilities.Task继承。 若要实现任务,请重写 Execute() 方法。 如果任务成功,
Execute
方法返回true
,否则false
。Task
实现 Microsoft.Build.Framework.ITask 并提供某些ITask
成员的默认实现,此外还提供一些日志记录功能。 请务必将状态输出到日志来诊断和排查任务,尤其是在出现问题并且任务必须返回错误结果(false
) 时。 出错时,类通过调用 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; }
任务 API 允许返回 false 来指示失败,而无需告知用户具体出了什么问题。 最好返回
!Log.HasLoggedErrors
而不是布尔代码,并在出现问题时记录错误。
日志错误
日志记录错误的最佳做法是在记录错误时提供详细信息,例如行号和不同的错误代码。 以下代码分析文本输入文件,并将 TaskLoggingHelper.LogError 方法与生成错误的文本文件中的行号一起使用。
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);
}
使用上一代码中显示的技术,文本输入文件的语法中的错误显示为生成错误,并提供有用的诊断信息:
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)
捕获任务中的异常时,请使用 TaskLoggingHelper.LogErrorFromException 方法。 这将改进错误输出,例如,获取引发异常的调用堆栈。
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;
}
此处未显示使用这些输入为生成的代码文件生成文本的其他方法的实现;请参阅示例存储库中的 AppSettingStronglyTyped.cs。
示例代码在生成过程中生成 C# 代码。 该任务就像任何其他 C# 类一样,因此,完成本教程后,可以对其进行自定义并添加你自己的方案所需的任何功能。
生成控制台应用并使用自定义任务
在本部分中,你将创建一个使用该任务的标准 .NET Core 控制台应用。
重要
请务必避免在同一 MSBuild 进程中生成将被该进程消耗的 MSBuild 自定义任务。 新项目应位于完全不同的 Visual Studio 解决方案中,或者新项目使用预生成的 dll 并从标准输出重新定位。
在新的 Visual Studio 解决方案中创建 .NET 控制台项目 MSBuildConsoleExample。
分发任务的正常方法是通过 NuGet 包,但在开发和调试期间,你可以将有关
.props
和.targets
的所有信息直接包含在应用程序的项目文件中,然后在将任务分发给其他人时移动到 NuGet 格式。修改项目文件以使用代码生成任务。 本节中的代码列表展示了在引用任务后修改的项目文件、设置任务的输入参数并编写用于处理清理和重建操作的目标,以便按预期删除生成的代码文件。
任务是使用 UsingTask 元素 (MSBuild) 注册的。
UsingTask
元素注册任务;它告知 MSBuild 任务的名称以及如何查找和运行包含任务类的程序集。 程序集路径是项目文件的相对路径。PropertyGroup
包含与任务中定义的属性对应的属性定义。 这些属性是使用属性设置的,任务名称用作元素名称。TaskName
是要从程序集中引用的任务的名称。 此属性应始终使用完全指定的命名空间。AssemblyFile
是程序集的文件路径。若要调用该任务,请将任务添加到相应的目标,在本例中
GenerateSetting
。目标
ForceGenerateOnRebuild
通过删除生成的文件来处理清理和重新生成操作。 将AfterTargets
属性设置为CoreClean
以使其在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>
注意
此代码使用对目标(BeforeTarget 和 AfterTarget)进行排序的另一种方法,而不是重写目标(如
CoreClean
)。 SDK 样式项目在项目文件的最后一行之后隐式导入目标;这意味着,除非手动指定导入,否则不能替代默认目标。 请参阅重写预定义目标。Inputs
和Outputs
属性通过提供增量生成的信息来帮助 MSBuild 更高效。 输入的日期与输出进行比较,以查看是否需要运行目标,或者是否可以重复使用上一个版本的输出。使用定义为已发现的扩展名创建输入文本文件。 使用默认扩展,在根目录中创建
MyValues.mysettings
,内容如下:Greeting:string:Hello World!
再次生成,应创建并生成已生成的文件。 检查 MySetting.generated.cs 文件的项目文件夹。
MySetting 的类 位于错误的命名空间中,因此现在请进行更改以使用应用命名空间。 打开项目文件并添加以下代码:
<PropertyGroup> <SettingNamespace>MSBuildConsoleExample</SettingNamespace> </PropertyGroup>
再次重新生成,并观察该类位于
MSBuildConsoleExample
命名空间中。 通过这种方式,您可以重新定义生成的类名(SettingClass
)、要用作输入的文本扩展文件(SettingExtensionFile
),以及它们的位置(RootFolder
),如果需要的话。打开
Program.cs
并更改硬编码的“Hello World!!” 更改为用户定义的常量:static void Main(string[] args) { Console.WriteLine(MySetting.Greeting); }
执行程序;它将打印生成的类中的问候语。
(可选)在生成过程中记录事件
可以使用命令行命令进行编译。 导航到项目文件夹。 你将使用 -bl
(二进制日志)选项生成二进制日志。 二进制日志将具有有用的信息,了解生成过程中发生的情况。
# 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
这两个命令都会生成日志文件 msbuild.binlog
,可以使用 MSBuild 二进制文件和结构化日志查看器打开。 选项 /t:rebuild
表示执行重新构建目标。 它将强制重新生成生成的代码文件。
祝贺! 你已生成一个生成代码的任务,并在生成中使用它。
打包任务以供分发
如果只需要在几个项目或单个解决方案中使用自定义任务,将任务作为原始程序集使用可能已足够,但为了在其他地方使用或与他人分享,最好的方法是将任务打包为 NuGet 包。
MSBuild 任务包与库 NuGet 包有一些主要区别:
- 他们必须捆绑自己的程序集依赖项,而不是将这些依赖项公开给消费项目。
- 它们不会将任何必需的程序集打包到
lib/<target framework>
文件夹中,因为这将导致 NuGet 将程序集包含在使用任务的任何包中 - 它们只需要针对 Microsoft.Build 程序集 编译 - 在运行时,这些程序集将由实际的 MSBuild 引擎提供,因此无需包含在包中
- 它们生成一个特殊的
.deps.json
文件,帮助 MSBuild 以一致的方式加载任务的依赖项(尤其是本机依赖项)
若要实现所有这些目标,必须对标准项目文件进行一些更改,这些更改不仅限于您可能已经熟悉的那些。
创建 NuGet 包
创建 NuGet 包是将自定义任务分发给其他人的建议方法。
准备生成包
若要准备生成 NuGet 包,请对项目文件进行一些更改,以指定描述包的详细信息。 创建的初始项目文件类似于以下代码:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="17.0.0" />
</ItemGroup>
</Project>
若要生成 NuGet 包,请添加以下代码以设置包的属性。 可以在 Pack 文档中看到受支持的 MSBuild 属性的完整列表:
<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>
需要 CopyLocalLockFileAssemblies 的属性,以确保依赖项复制到输出目录。
将依赖项标记为私有
MSBuild 任务的依赖项必须包含在包内,它们不能表示为普通的包引用。 该包不会向外部用户公开任何常规依赖项。 这需要执行两个步骤来完成:将程序集标记为私有,并实际将它们嵌入生成的包中。 在这个示例中,我们假设你的任务依赖于 Microsoft.Extensions.DependencyInjection
才能工作,因此请在版本 6.0.0
中添加 PackageReference
和 Microsoft.Extensions.DependencyInjection
。
<ItemGroup>
<PackageReference
Include="Microsoft.Build.Utilities.Core"
Version="17.0.0" />
<PackageReference
Include="Microsoft.Extensions.DependencyInjection"
Version="6.0.0" />
</ItemGroup>
现在,使用 PrivateAssets="all"
属性标记此任务项目的每个依赖项,PackageReference
和 ProjectReference
。 这会告知 NuGet 不向使用项目公开这些依赖项。 可以在 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>
将依赖项捆绑到包中
还必须将依赖项的运行时资产嵌入到任务包中。 这分为两个部分:一个 MSBuild 目标(用于将依赖项添加到 BuildOutputInPackage
ItemGroup)和几个属性(用于控制这些 BuildOutputInPackage
项的布局)。 可以在 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>
不要捆绑 Microsoft.Build.Utilities.Core 程序集
如上所述,此依赖项将在运行时由 MSBuild 本身提供,因此无需将其捆绑到包中。 为此,请将 ExcludeAssets="Runtime"
特性添加到它的 PackageReference
...
<PackageReference
Include="Microsoft.Build.Utilities.Core"
Version="17.0.0"
PrivateAssets="all"
ExcludeAssets="Runtime"
/>
...
生成并嵌入 deps.json 文件
MSBuild 可以使用 deps.json
文件来确保加载正确的依赖项版本。 需要添加一些 MSBuild 属性来生成文件,因为默认情况下不会为库生成该文件。 然后,添加一个目标以将其包含在包输出中,这与包依赖项的实现方式类似。
<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>
在包中包含 MSBuild 属性和目标
有关本部分的背景信息,请阅读 属性和目标,以及如何 在 NuGet 包中包含属性和目标。
在某些情况下,你可能希望在使用包的项目中添加自定义生成目标或属性,例如在生成过程中运行自定义工具或进程。 为了达到这一目的,你需要将 <package_id>.targets
或 <package_id>.props
格式的文件放入项目中的 build
文件夹。
项目根 生成 文件夹中的文件被视为适用于所有目标框架。
在本部分中,你将关联 .props
和 .targets
文件中的任务实现,这些文件将包含在 NuGet 包中,并自动从引用项目加载。
在任务的项目文件中,AppSettingStronglyTyped.csproj,添加以下代码:
<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>
创建一个 生成的 文件夹,并在该文件夹中添加两个文本文件:
AppSettingStronglyTyped.props
和 AppSettingStronglyTyped.targets。AppSettingStronglyTyped.props
在 Microsoft.Common.props最初阶段导入,之后定义的属性对其不可用。 因此,请避免引用尚未定义的属性;它们计算结果为空。从 NuGet 包导入
.targets
文件后,会从 Microsoft.Common.targets 导入 Directory.Build.targets。 因此,它可以覆盖大部分构建逻辑中定义的属性和目标,或者为所有项目设置属性,而不受各个项目配置的影响。 请参阅导入顺序。AppSettingStronglyTyped.props 包括任务,并定义一些具有默认值的属性:
<?xml version="1.0" encoding="utf-8" ?> <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <!--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>
安装包时,会自动包含
AppSettingStronglyTyped.props
文件。 然后,客户端具有可用的任务和一些默认值。 但从未使用过。 为了将此代码投入使用,请在AppSettingStronglyTyped.targets
文件中定义一些目标,安装包时也会自动包含这些目标:<?xml version="1.0" encoding="utf-8" ?> <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <!--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>
第一步是创建 ItemGroup,它表示要读取的文本文件(可能不止一个),这将是我们的一些任务参数。 我们查找的位置和扩展有默认值,但你可以替代在客户端 MSBuild 项目文件中定义属性的值。
然后定义两个 MSBuild 目标。 我们将扩展 MSBuild 过程,从而重写预定义的目标:
BeforeCompile
:目标是调用自定义任务以生成类并包括要编译的类。 在完成核心编译之前,插入此目标中的任务。 输入和输出字段与 增量构建相关。 如果所有输出项都 up-to-date,MSBuild 将跳过目标。 目标的这种增量生成可以显著提高生成的性能。 如果某项的输出文件的日期与其输入文件或文件相同或更新,则将其视为up-to状态。AfterClean
:目标是在发生常规清理后删除生成的类文件。 调用核心清理功能后,将插入此目标中的任务。 它强制在重新生成目标执行时重复代码生成步骤。
生成 NuGet 包
若要生成 NuGet 包,可以使用 Visual Studio(右键单击 解决方案资源管理器中的项目节点,然后选择 包)。 也可以使用命令行执行此操作。 导航到任务项目文件 AppSettingStronglyTyped.csproj
所在的文件夹,并执行以下命令:
// -o is to define the output; the following command chooses the current folder.
dotnet pack -o .
祝贺! 已生成名为 \AppSettingStronglyTyped\AppSettingStronglyTyped\AppSettingStronglyTyped.1.0.0.nupkg的 NuGet 包。
包具有扩展 .nupkg
,并且是压缩的 zip 文件。 可以使用 zip 工具打开它。 .target
和 .props
文件位于 build
文件夹中。 .dll
文件位于 lib\netstandard2.0\
文件夹中。 AppSettingStronglyTyped.nuspec
文件位于根级别。
(可选)支持多目标
应考虑同时支持 Full
(.NET Framework)和 Core
(包括 .NET 5 及更高版本)MSBuild 分发,以支持尽可能广泛的用户群。
对于“普通”的.NET SDK 项目,多重目标框架意味着在项目文件中设置多个 TargetFrameworks。 执行此操作时,将为两个 TargetFrameworkMoniker 触发生成,并且可以将整体结果打包为单个项目。
这不是 MSBuild 的完整故事。 MSBuild 有两种主要交付车辆:Visual Studio 和 .NET SDK。 这些是截然不同的运行时环境;一个在 .NET Framework 运行时上运行,另一个在 CoreCLR 上运行。 这意味着,虽然代码可以面向 netstandard2.0,但任务逻辑可能根据当前正在使用的 MSBuild 运行时类型存在差异。 实际上,由于 .NET 5.0 及更高版本中许多新的 API,因此为 MSBuild 任务源代码设定多个 TargetFrameworkMoniker 目标以及为 MSBuild 目标逻辑设定多个 MSBuild 运行时类型目标都是有意义的。
多目标设定所需的更改
以多个 TargetFrameworkMoniker (TFM) 为目标:
更改项目文件以使用
net472
和net6.0
这两个 TFM(后者可能会根据您要定位的 SDK 级别进行更改)。 在 .NET Core 3.1 退出支持之前,你可能希望以netcoreapp3.1
为目标。 执行此操作时,包文件夹结构从tasks/
更改为tasks/<TFM>/
。<TargetFrameworks>net472;net6.0</TargetFrameworks>
更新
.targets
文件以使用正确的 TFM 加载任务。 所需的 TFM 将根据上面选择的 .NET TFM 而更改,但对于以net472
和net6.0
为目标的项目,将具有如下属性:
<AppSettingStronglyTyped_TFM Condition=" '$(MSBuildRuntimeType)' != 'Core' ">net472</AppSettingStronglyTyped_TFM>
<AppSettingStronglyTyped_TFM Condition=" '$(MSBuildRuntimeType)' == 'Core' ">net6.0</AppSettingStronglyTyped_TFM>
此代码使用 MSBuildRuntimeType
属性作为活动托管环境的代理。 设置此属性后,可以在 UsingTask
中使用它来加载正确的 AssemblyFile
:
<UsingTask
AssemblyFile="$(MSBuildThisFileDirectory)../tasks/$(AppSettingStronglyTyped_TFM)/AppSettingStronglyTyped.dll"
TaskName="AppSettingStrongTyped.AppSettingStronglyTyped" />
后续步骤
许多任务涉及调用可执行文件。 在某些情况下,可以使用 Exec 任务,但如果 Exec 任务的限制是个问题,也可以创建自定义任务。 以下教程将演练这两个选项,其中包含更真实的代码生成方案:创建自定义任务以生成 REST API 的客户端代码。
或者,了解如何测试自定义任务。