.NET projects have pragmatic default settings, but these won’t always be exactly what you want in your own projects. For example, at endjin we like clean builds, and to remove the temptation to check in code that produces compiler warnings we configure the compiler to report errors as warnings; for legal reasons we want a copyright header in all of the open source code we publish to GitHub, so we configure StyleCop to insist on this.
We have a few policies like these that require us to deviate from the SDK’s defaults. It’s not that the SDK is wrong here—it would not be reasonable to impose our defaults on the rest of the world. But we do want to apply them to all of our own code.
So how can you get the defaults you want across all your projects? You can obviously just add the settings you want in every project file, but that’s tedious and error-prone. The next-simplest approach is to put these settings into a file, and then add a line to every project that imports that file. This works a lot better, because it requires only a single line in each project, and it ensures that all the projects in a solution are using the same settings because those settings are defined in a single place.
You might even be able to avoid adding the
<Import> line to each project file by creating
Directory.Build.targets files. If you put files with these names in the same folder as your
.sln file, they will be incorporated automatically into every project, running before and after the contents of your project file respectively.
However, this approach has issues if you have multiple solutions. Endjin maintains numerous projects across various repositories, and the problem that arises with the simple “copy some files into the project” approach is that there are no good versioning or update mechanisms. What do you do if you discover that an update to the .NET SDK requires you to change something? (That’s happened to us in the last week.) Or perhaps you want to introduce a new policy that requires a change to the common files.
You can go through every project and update these files by hand, but this is tedious and error-prone, and tedious and error-prone procedures are exactly what we’re trying to avoid. (Are you tired of me writing “tediuos and error-prone” yet?) Moreover, when applying these kinds of updates, you discover that some projects have made their own customizations to these files, leaving you with two problems: 1) how to merge in the updates to the common files without breaking the customizations; 2) deciding whether the modifications made by this project are something you want to push out to everyone else because they are generally useful.
If only .NET had some sort of system for publishing sets of files intended for use in multiple projects in a way that supports versioning and straightforward distribution of updates…
NuGet build assets
NuGet is designed for exactly this kind of scenario. It might be used mostly for distributing DLLs, but it’s perfectly capable of sharing other kinds of files. Moreover, the .NET build system has a feature designed specifically for solving the very problem at hand: a NuGet package can include files that get incorporated into your build.
Some Microsoft packages rely on this. The Azure Functions .NET SDK needs to perform some build steps specific to Azure Functions. (For example, it needs to generate a JSON file describing each of the triggers in your code.) We can see how it’s able to do this by opening up
Microsoft.NET.Sdk.Functions.nupkg in the NuGet package explorer application. This is what v3.0.4 looks like:
The first thing to notice about this is that there’s no
lib folder. In most NuGet packages, the
lib folder is the entire point of the package—we usually import NuGet packages to add some DLLs to our project, and that folder is where those DLLs live. But this package does not make any DLLs directly available to your project—all the types that support Azure Function development actually come from other packages that
Microsoft.NET.Sdk.Functions.nupkg references (e.g.
This package does contain some DLLs, but they are all in the
tools folder, the contents of which will not be available to your project’s code; this folder is for things used at development time. These particular DLLs contain the code required to perform the extra build-time work needed in Azure Functions projects.
The presence of this
tools folder does not in itself cause anything interesting to happen at build time. The most important folder here is the
build folder. If a .NET project has a reference to a package that contains a
build folder, the SDK will check for two files in that folder:
<PackageName> is the name of the NuGet package, i.e.
Microsoft.NET.Sdk.Functions in this example). Either or both may be present. The
.props file, if present, will be processed before your project file’s contents, and the
.targets file, if present, will be processed after your project file’s contents. (So these are very similar in nature to the
Directory.Build.targets files mentioned earlier, it’s just that the files live in a NuGet package instead of a particular solution folder.)
You might have noticed that this example has more than those two files. How do all the other files come into play? Well the two files that the SDK picks up—
<Import> elements that refer to those other ones. This seems to be a fairly common idiom: packages that do anything non-trivial during the build typically split this work into multiple files, and then the two files that the SDK goes looking for just orchestrate the execution of these other files.
The Functions SDK defines custom MSBuild tasks (e.g., for the aforementioned JSON file generation) which is why it has that
tools folder, but that’s not processed automatically by the .NET SDK. The
Microsoft.NET.Sdk.Functions.targets file contains various
<UsingTask> lines that load these DLLs explicitly: https://github.com/Azure/azure-functions-vs-build-sdk/blob/3.0.4/src/Microsoft.NET.Sdk.Functions.MSBuild/Targets/Microsoft.NET.Sdk.Functions.targets#L14-L31
If you want to see a simpler example, endijn’s standard settings are up at https://github.com/endjin/Endjin.RecommendedPractices.NuGet/. Currently, we have no compiled code in this package, only
NuGet development dependencies
One thing we don’t want to do is cause anyone taking a dependency on our OSS projects to end up with a dependency on our
Endjin.RecommendedPractices NuGet package. (You’re more than welcome to adopt them by choice, we just don’t want to impose them as a condition of use!)
Normally, anything that has an explicit dependency on some NuGet package ends up with an implicit dependency on anything that package depends on. For example, our Corvus.Retry package depends on
Endjin.RecommendedPractices, so you might expect that a dependency on
Corvus.Retry would result in an implicit dependency on
Endjin.RecommendedPractices. Not so.
nuspec file in
Endjin.RecommendedPractices includes this entry:
<developmentDependency>true</developmentDependency>. This tells NuGet that nothing in this package is required at runtime—it exists purely to support development-time operations. This has the effect of disabling the normal implicit dependency behaviour, meaning you can safely add a package of this kind to your project without any impact on anyone using your project.