It’s often useful to create a custom task for your use, but consuming it presents some complexity.
The most common use of custom tasks is via NuGet package. You can package and publish a .nupkg
to a NuGet feed from another repo or your primary repo and then reference it via the usual NuGet approaches.
This is the best way to produce a task for others to consume. However, if your repo is the only consumer of a task this can be inconvenient:
- It requires a separate build/publish process.
- It’s hard to incrementally test the task (you have to produce a package and push or override the package in your NuGet cache).
- If you go too long between task modifications, you might forget how to publish it.
https://docs.microsoft.com/visualstudio/msbuild/msbuild-inline-tasks
These are convenient and easy to author, but hard to debug, can be inefficient, and can be difficult to configure to use custom references (for instance if you need the task to call into one of your own libraries).
You can also build a task assembly as a regular assembly in your solution. This is slightly harder to use (a simple NuGet reference no longer suffices), but gets you the benefits of a compiled task assembly (speed, debuggability, ease of reference) while avoiding the difficulty of NuGet packages (packaging step, reference updates).
UsingTask
elements are evaluated before targets start executing in a project. Because of that, you cannot use a ProjectReference
to get the path to a task assembly that is the output of another project. Instead, you must reference the task in two steps:
- Include a
UsingTask
element.- Use properties to construct an
AssemblyFile
attribute that points to the output of the task project. - Specify
TaskFactory="TaskHostFactory"
to force MSBuild to load a new copy of the assembly in a new process for every execution of the task, allowing the task to be updated in the same build that uses it.
- Use properties to construct an
- Include a
ProjectReference
to ensure that the task assembly is built before it is used.- Set
ReferenceOutputAssembly="false"
because the task assembly only needs to be built, not referenced by the calling project. - TODO: multitargeting compatibility (
SetTargetFramework
conditional on$(MSBuildRuntimeType)
?)
- Set
While UsingTask
elements are considered before running tasks, the assembly file referenced in the UsingTask
is not loaded until the first use of a task within it, so it's ok for the task assembly to not yet be built when the UsingTask
is initially evaluated.
If your task will be used in more than one project, define the UsingTask
, the Target
that uses it, and the ProjectReference
in a .targets
file and import it in the projects where you will use it.
By default, MSBuild uses normal .NET Framework assembly load behavior, which causes the task assembly DLL to be loaded into an MSBuild.exe
process that may be long-lived. That can cause failures in subsequent builds, because the task assembly can't be overwritten. Fortunately, MSBuild can be configured to run a task in its own short-lived process, and you should use that option for tasks built within the same solution.