Why does my .NET Standard NuGet package trigger so many dependencies?

Tom Wright picture Tom Wright · Nov 18, 2017 · Viewed 7.8k times · Source

I've been mucking about with a .NET Standard project and NuGet. I've got a working project and have uploaded it to NuGet.org. My project targets .NET Standard 1.3, which should support .NET Framework 4.6 and .NET Core 1.0.

But when I tried to add my project (via NuGet) to a fresh .NET Framework 4.6 project, the dependencies resolved to 47 packages! They're all system libraries and appear to be dependencies of either Microsoft.NETCore.Platforms or NETStandard.Library 1.6.1. (Gist of full PM output.)

My project only imports (using) a handful of libraries, none of which I added manually; i.e. they're all libraries that "came with" .NET Standard. These libraries are:

  1. System
  2. System.Text
  3. System.Reflection
  4. System.Linq
  5. System.Collections.Generic;

The thing is, I decided to make my project target .NET Standard because I wanted it to work seamlessly across .NET Framework and .NET Core applications. I thought the whole point of Standard was to set a minimum level of compatibility. By extension, I suppose I had assumed (perhaps erroneously) that libraries like System.Console would be automatically available in either Core or Framework.

I didn't notice anything like this when I tested my Standard project as a dependency in a Framework and Core project within the same solution, so I'm suspicious that this might be a NuGet thing.

What's really going on here? And how can I make my .NET Standard library available on NuGet without a huge list of dependencies?

Is it a problem with the way I've specified my NuGet package? Or have I fundamentally misunderstood something?

Answer

Martin Ullrich picture Martin Ullrich · Nov 18, 2017

You haven't done anything wrong, this is expected to happen. If you want nothing more than your own DLL being added to a new .NET Framework project, you have to target .NET Standard 2.0 for your library wait for a .NET Framework version that natively supports both the API and assembly versions - which is going to be 4.7.2 (while .NET Framework 4.7.1 supports all the APIs, there were bugs with how some assemblies are versioned and so the tooling (VS 2017 15.5+) will add additional assemblies to fix that).

What you are seeing are side effects of how .NET Standard is built and the support for the supported frameworks is implemented. This is also different based on the .NET Standard version you target and the tooling used to reference the library package.

In .NET Standard < 2.0, you reference the NETStandard.Library meta-package which in turn references additional (System.*) packages. Those packages contain the reference assemblies that make up the ".NET Standard Contract" - a set of APIs and the assembly names + versions.

When the NuGet package you create for .NET Standard 1.0-1.6 is then referenced by an application, these individual packages don't bring in the reference assemblies but rather implementation assemblies for the framework that the application targets.

For .NET Core, these match the assemblies that are already part of the runtime so the DLL files won't end up next to the built application. This changed however when a new set of packages was released for .NET Core 1.1 (NETStandard.Library version 1.6.1). This resulted in applications built for .NET Core 1.0 ending up getting newer implementation assemblies that were meant to be included in .NET Core 1.1 (luckily, 1.1 was then made the "long-term support" version since that sparked a discussion about which assemblies are part of the LTS promise).

On .NET Framework these libraries (with some exceptions like System.Net.Http) don't do much - they just forward to the system assemblies. So for example the "contract" defines that System.Object is defined in a System.Runtime.dll assembly. So the System.Runtime.dll file you end up with in a .NET Framework application contains a System.Runtime.dll that contains type forward to .NET Framework's mscorlib.dll. .NET Core already contains a different System.Runtime.dll that does something different for that platform. This mechanism allows for a single DLL file to work on both platforms since those type forwards and additional implementations assure the same "contract" (types + assemblies + assembly versions) working on both implementations.

.NET Standard 2.0 aimed to reduce the number of packages and DLLs being necessary and also to remove requiring updates to NETStandard.Library whenever a new .NET Core version is released.

So for .NET Standard 2.0 and .NET Core 2.0, the NETStandard.Library package only brings reference assemblies for compiling code to a project, but the resulting NuGet package no longer depends on this package. So when you create a library targeting .NET Standard 2.0 and publish it, it will have no NuGet dependencies (unless you add additional ones).

The logic of what "support libraries" to bring in when consuming a .NET Standard library was moved to the tooling that is used during build. So when a library that contains a reference to a netstandard.dll is added to a .NET Framework project, the tooling will then add necessary support DLLs based on the version of .NET Framework being used. This was done for .NET Standard 2.0 as well as .NET Standard 1.5+ since .NET Framework 4.6.1 was retroactively made compatible with .NET Standard 2.0 (was 1.4 previously) through these kinds of DLL files. The same tooling also makes sure that even if NuGet packages are somehow brought in to such an application project, any .NET Standard implementation libraries brought in via NuGet are removed from the build. So if you reference a .NET Standard 1.0 NuGet package that was built when .NET Core 1.0 was released, all its NuGet dependencies are trimmed out and you get the support libraries shipped with the build tooling instead.

The idea was that .NET Framework 4.7.1 would contain all the necessary assemblies "inbox" so that a netstandard.dll, System.Runtime.dll etc. are part of .NET Framework and any .NET Standard 1.0-2.0 DLL file would "just work", the problem was that these "inbox" dll files had a too low version number for some assemblies so libraries would fail to load - this was fixed by changing the tooling again to include DLL files with higher version numbers as support libraries which in turn forward to the "inbox" .NET Framework assemblies. This is planned to be fixed in .NET Framework 4.7.2.