Учебник. Создание настраиваемой задачи для создания кода

В этом учебнике вы создадите настраиваемую задачу в MSBuild в C#, которая обрабатывает создание кода, а затем будете использовать эту задачу в сборке. В этом примере показано, как использовать MSBuild для выполнения операций очистки и перестройки. В примере также показано, как поддерживать добавочную сборку, чтобы код создавался только при изменении входных файлов. Продемонстрированные методики применимы к широкому спектру сценариев создания кода. Кроме того, показано использование NuGet для упаковки задачи для распространения, а в дополнительном шаге используется средство просмотра BinLog для повышения эффективности процесса устранения неполадок.

Необходимые компоненты

Вы должны быть знакомы с основными понятиями MSBuild, такими как задачи, целевые объекты и свойства. См. Основные понятия MSBuild.

Для этих примеров требуется платформа MSBuild. Она устанавливается вместе с Visual Studio, но ее также можно установить отдельно. См. Скачивание MSBuild без Visual Studio.

Введение в пример кода

В примере принимается входной текстовый файл, содержащий значения, которые необходимо задать, и создается файл кода C# с кодом, который создает эти значения. Хотя это простой пример, эти же методы можно применять к более сложным сценариям создания кода.

В этом руководстве вы создадите настраиваемую задачу MSBuild с именем AppSettingStronglyTyped. Задача будет считывать набор текстовых файлов, и каждый файл будет содержать строки в следующем формате:

propertyName:type:defaultValue

Код создает класс C# со всеми константами. В случае возникновения проблемы сборка должна остановиться, а пользователю должно быть предоставлено достаточно информации для диагностики проблемы.

Полный пример кода для этого учебника приведен в репозитории для .NET на GitHub: Настраиваемая задача — создание кода.

Создание проекта AppSettingStronglyTyped

Создайте библиотеку классов .NET Standard. Используйте платформу .NET Standard 2.0.

Обратите внимание на разницу между полной версией MSBuild (тем, что использует Visual Studio) и переносимой версией MSBuild, которая включена в командную строку .NET Core.

  • Полная версия MSBuild обычно находится внутри Visual Studio. Запускается на .NET Framework. Visual Studio использует ее при выполнении сборки в решении или проекте. Эта версия также доступна в среде командной строки, такой как Командная строка разработчика Visual Studio или PowerShell.
  • MSBuild .NET включена в командную строку .NET Core. Она выполняется в .NET Core. Visual Studio не вызывает эту версию MSBuild напрямую. Она поддерживает только те проекты, сборка которых выполняется с помощью Microsoft.NET.Sdk.

Если вы хотите совместно использовать код между .NET Framework и любой другой реализацией .NET, например .NET Core, ваша библиотека должна быть нацелена на .NET Standard 2.0 и должна выполняться внутри Visual Studio на платформе .NET Framework. .NET Framework на поддерживает .NET Standard 2.1.

Создание настраиваемой задачи MSBuild под названием AppSettingStronglyTyped

Первым шагом является создание настраиваемой задачи MSBuild. Сведения о том, как написать настраиваемую задачу MSBuild, могут помочь вам понять следующие шаги. Настраиваемая задача MSBuild — это класс, реализующий интерфейс ITask.

  1. Добавьте ссылку на пакет NuGet Microsoft.Build.Utilities.Core, а затем создайте класс с именем AppSettingStronglyTyped, производный от Microsoft.Build.Utilities.Task.

  2. Добавьте три свойства. Эти свойства определяют параметры задачи, которую пользователи задают при использовании задачи в клиентском проекте:

     //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; }
    
  3. При создании настраиваемой задачи вы наследуете от 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;
     }
    

    Task 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 из стандартных выходных данных.

  1. Создайте консольный проект .NET MSBuildConsoleExample в новом решении Visual Studio.

    Обычным способом распространения задачи является пакет NuGet, но во время разработки и отладки можно включить всю информацию в .props и .targets непосредственно в файл проекта приложения, а затем перейти к формату NuGet при распространении задачи среди других пользователей.

  2. Измените файл проекта, чтобы использовать задачу создания кода. В коде в этом разделе показан измененный файл проекта после добавления ссылки на задачу, задание входных параметров для задачи и записи целевых объектов для обработки операций очистки и перестройки, чтобы созданный файл кода был удален ожидаемым образом.

    Задачи регистрируются с помощью элемента UsingTask (MSBuild). Элемент UsingTask регистрирует задачу. Он сообщает MSBuild имя задачи, а также о том, как определить и запустить сборку, содержащую класс задачи. Путь к сборке задается относительно файла проекта.

    PropertyGroup содержит определения свойств, соответствующие свойствам, определенным в задаче. Эти свойства задаются с помощью атрибутов, а имя задачи используется в качестве имени элемента.

    TaskName — это имя задачи для создания ссылки из сборки. Этот атрибут должен всегда содержать полностью указанные пространства имен. AssemblyFile — это путь к файлу сборки.

    Чтобы вызвать задачу, добавьте ее в соответствующий целевой объект, в данном случае GenerateSetting.

    Целевой объект ForceGenerateOnRebuild обрабатывает операции очистки и перестройки, удаляя созданный файл. Чтобы он запускался после целевого объекта CoreClean, атрибуту AfterTargets присваивается значение 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>
    

    Примечание.

    Вместо переопределения целевого объекта, такого как CoreClean, этот код использует другой способ упорядочивания целевых объектов (BeforeTarget и AfterTarget). Проекты в стиле SDK имеют неявный импорт целевых объектов после последней строки файла проекта. Это означает, что нельзя переопределить целевые объекты по умолчанию, если вы не укажете импорт вручную. См. Переопределение предопределенных целевых объектов.

    Атрибуты Inputs и Outputs помогают повысить эффективность MSBuild, предоставляя сведения для добавочных сборок. Даты входных данных сравниваются с выходными данными, чтобы определить, нужно ли выполнить целевой объект или использовать выходные данные предыдущей сборки повторно.

  3. Создайте входной текстовый файл с расширением, определенным для обнаружения. Используя расширение по умолчанию, создайте MyValues.mysettings в корне, используя следующее содержимое:

     Greeting:string:Hello World!
    
  4. Повторите сборку. Созданный файл должен быть создан и скомпилирован. Проверьте папку проекта для файла MySetting.generated.cs.

  5. Класс MySetting находится в неправильном пространстве имен, поэтому теперь внесите изменения, чтобы использовать наше пространство имен приложения. Откройте файл проекта и добавьте следующий код:

     <PropertyGroup>
     	<SettingNamespace>MSBuildConsoleExample</SettingNamespace>
     </PropertyGroup>
    
  6. Повторно выполните сборку и убедитесь, что класс находится в пространстве имен MSBuildConsoleExample. Таким образом вы при желании сможете переопределить созданное имя класса (SettingClass), расширение текстовых файлов (SettingExtensionFile), которые будут использоваться в качестве входных данных, и расположение для них (RootFolder).

  7. Откройте программу 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, добавьте следующий код, чтобы задать свойства пакета. Вы можете увидеть полный список поддерживаемых свойств 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>
	...
</PropertyGroup>

Пометка зависимостей как частных

Зависимости вашей задачи MSBuild должны быть упакованы внутри пакета; они не могут быть выражены как обычные ссылки на пакеты. Пакет не будет предоставлять обычные зависимости внешним пользователям. Для этого требуется два шага: пометить ваши сборки как частные и фактически встроить их в сгенерированный пакет. В этом примере мы предполагаем, что ваша задача зависит от работы Microsoft.Extensions.DependencyInjection, поэтому добавьте PackageReference к Microsoft.Extensions.DependencyInjection в версии 6.0.0.

<ItemGroup>
	<PackageReference 
		Include="Microsoft.Build.Utilities.Core"
		Version="17.0.0" />
	<PackageReference
		Include="Microsoft.Extensions.DependencyInjection"
		Version="6.0.0" />
</ItemGroup>

Теперь отметьте каждую зависимость этого проекта Task (PackageReference и ProjectReference) с помощью атрибута PrivateAssets="all". Так 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>

Объединение зависимостей в пакет

Вы также должны внедрить ресурсы среды выполнения наших зависимостей в пакет Task. Здесь есть две части: целевой объект MSBuild, который добавляет наши зависимости в ItemGroup BuildOutputInPackage, и несколько свойств, управляющих расположением этих элементов 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>
	...
</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

Файл deps.json может использоваться MSBuild для обеспечения загрузки правильных версий ваших зависимостей. Вам потребуется добавить некоторые свойства 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 проекта.

Файлы в корневой папке build считаются пригодными для всех требуемых версий .NET Framework.

В этом разделе вы заключите реализацию задачи в файлы .props и .targets, которые будут включены в пакет NuGet и будут автоматически загружаться из ссылающегося проекта.

  1. В файл проекта задачи 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>
    
  2. Создайте папку build и добавьте в нее два текстовых файла: AppSettingStronglyTyped.props и AppSettingStronglyTyped.targets. AppSettingStronglyTyped.props импортируется в Microsoft.Common.props рано, поэтому определенные позднее свойства ему недоступны. Так что не ссылайтесь на свойства, которые еще не определены — они будут оценены как пустые.

    Directory.Build.targets импортируется из Microsoft.Common.targets после импорта .targets файлов из пакетов NuGet. Таким образом, в нем могут быть переопределены свойства и целевые объекты, определенные в большей части логики сборки, а также заданы свойства для всех проектов, которые будут иметь приоритет над настройками отдельных проектов. См. порядок импорта.

    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>
    
  3. Файл 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: целью является вызов настраиваемой задачи для создания класса и включения класса для компиляции. Задачи в этом целевом объекте вставляются до завершения базовой компиляции. Поля ввода и вывода связаны с инкрементной сборкой. Если все выходные элементы актуальны, MSBuild пропускает этот целевой объект. Такая инкрементная сборка целевого объекта может значительно ускорить сборку. Элемент считается актуальным, если возраст его выходного файла меньше или равен возрасту его входных файлов.

    • AfterClean: цель заключается в удалении созданного файла класса после выполнения общей очистки. Задачи в этом целевом объекте вставляются после вызова основной функции очистки. При выполнении целевого объекта перестройки происходит принудительное повторение шага создания кода.

Создание пакета NuGet

Чтобы создать пакет NuGet, можно использовать Visual Studio (щелкните правой кнопкой мыши узел проекта в Обозревателе решений и выберите Пакет). Это также можно сделать с помощью командной строки. Перейдите в папку, где находится файл проекта задачи AppSettingStronglyTyped.csproj, и выполните следующую команду:

// -o is to define the output; the following command chooses the current folder.
dotnet pack -o .

Поздравляем! Вы создали пакет NuGet с именем \AppSettingStronglyTyped\AppSettingStronglyTyped\AppSettingStronglyTyped.1.0.0.nupkg.

Пакет имеет расширение .nupkg и является сжатым ZIP-файлом. Его можно открыть с помощью программы для работы с ZIP-файлами. Файлы .target и .props находятся в папке build. Файл .dll находится в папке lib\netstandard2.0\. Файл AppSettingStronglyTyped.nuspec находится на корневом уровне.

(Необязательно.) Поддержка настройки для различных версий

Вам следует рассмотреть возможность обеспечения поддержки дистрибутивов MSBuild Full (.NET Framework) и Core (включая .NET 5 и более поздние версии), чтобы также обеспечить поддержку максимально широкой пользовательской базы.

Для "обычных" проектов .NET SDK настройка для различных версий означает настройку нескольких экземпляров TargetFrameworks в файле проекта. Когда вы это сделаете, сборки будут запущены для обоих экземпляров TargetFrameworkMonikers, а общие результаты могут быть упакованы как один артефакт.

Этим дело не ограничивается для MSBuild. В MSBuild есть два основных транспортных средства доставки: Visual Studio и пакет SDK для .NET. Это очень разные решения: одно работает в среде выполнения платформа .NET Framework, а другое — в CoreCLR. Это означает, что, хотя ваш код может быть настроен для netstandard2.0, логика вашей задачи может отличаться в зависимости от того, какой тип среды выполнения MSBuild сейчас используется. На практике, так как в .NET 5.0 и более поздних версиях появилось очень много новых API, имеет смысл использовать как настройку исходного кода задачи MSBuild для разных экземпляров TargetFrameworkMonikers, так и настройку целевой логики MSBuild для разных типов среды выполнения MSBuild.

Изменения, необходимые для настройки для различных версий

Для настройки для разных экземпляров TargetFrameworkMonikers (TFM):

  1. Настройте файл проекта на использование TFM net472 и net6.0 (последний экземпляр может изменяться с учетом целевого уровня пакета SDK). Возможно, вы не будете выполнять настройку для netcoreapp3.1 до прекращения поддержки .NET Core 3.1. Когда вы это сделаете, структура папок пакета изменится с tasks/ на tasks/<TFM>/.

    <TargetFrameworks>net472;net6.0</TargetFrameworks>
    
  2. Обновите файлы .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.

Кроме того, вы можете узнать, как проверить настраиваемую задачу.