A few months ago, I wrote demos and a blog post about writing MSBuild tasks and shipping them in a NuGet package. The question I’ve been asked most frequently since then is “how to I make a task with external dependencies work?” i.e. “my task needs to connect to MySql, Postgres, load SQLite”, or something like that. When I started writing a post to answer these questions, I intended to show you the way to make all this work.

But then, as the list of workarounds grew, I realized that there is a simpler solution that works just as well and avoids the headaches. So in response to this, I wrote Bundling .NET build tools in NuGet. This approach shows you how to use a regular console app instead of an MSBuild task to accomplish the same kinds of things.

This is a follow up to Shipping a cross-platform MSBuild task in a NuGet package.

Just don’t

TL;DR Just don’t. Use a console tool instead. MSBuild tasks have limitations. It’s typically easier to write a console tool and start a new process from MSBuild using Exec.

The exception

If you need access to MSBuild’s object model so you can use logging, items, properties, and task outputs, you will still need to write a task. But even in this case, I recommend making a task that acts as a wrapper for invoking the console tool.

The ToolTask API in MSBuild is designed for exactly this. See an example implementation here: ToolTask implementation.

But if you insist…

Workarounds

MSBuild tasks aren’t well suited for dependencies beyond the task assembly itself. Although there is an issue open on MSBuild to improve this (see Microsoft/MSBuild#1312), if you’re reading this, you probably can’t wait for a future version of MSBuild.

Some issues you might run into:

Third-party dependencies and NuGet

Ideally, you would be able to put your MSBuild task in an NuGet library and let PackageReference do its magic to ensure your dependencies are installed.

But that’s not supported. See Microsoft/MSBuild#1755.

So instead, you have to make a “fat package”. This means you ship your third-party dependencies in your own NuGet package. See “BuildBundlerMinifier” as an example:

Binding redirects and version conflicts

Visual Studio and MSBuild.exe run on .NET Framework. If your assembly uses a dependency also used by MSBuild itself, you can end of with FileNotFoundException or MissingMethodException when the runtime fails to load the right version. Normally, this is resolved with binding redirects. This is not supported by MSBuild. To workaround this, you have to hook into assembly resolving hooks.

See this example: AssemblyResolver.cs

Native dependencies from tasks on .NET Framework

This is not supported by MSBuild either. See Microsoft/MSBuild#1887.

You can workaround by using a P/Invoke call to LoadLibraryEx to load a native library into memory. See this example: NativeLibraryLoader.cs.

Native dependencies from tasks on .NET Core

On .NET Core, you can workaround by implementing AssemblyLoadContext and overwriting LoadUnmanagedDll. See this example: ContextAwareTask.cs

Aligning versions with MSBuild

If you need a dependency that is also used in MSBuild itself, you have to align with the version MSBuild uses. This includes System.Reflection.Metadata, System.Collections.Immutable, NuGet, Newtonsoft.Json, and others. See example: dependencies.props

Closing

My recommendation: consider making your build task a console tool instead and use Exec to invoke it. For info on this approach, see Bundling build tools in MSBuild packages.