In a previous post I showed how to build for net4x on macOS, and it turns out you have everything you need to do it with the .NET Core SDK, except for the Reference Assemblies, and in that post I showed some workarounds.

This problem doesn't just exist on macOS however, it's also applicable when you are building on Linux, in Docker, or even on Windows without having the right targeting pack installed.

Now the dotnet team have made a more seamless way of doing this, with a new NuGet metapackage;

Microsoft.NETFramework.ReferenceAssemblies 1.0.0
Microsoft .NET Framework Reference Assemblies

How do I use it?

Just add this to your project file:

<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />

The team are talking about adding this to the SDK imports, so in future it may just already be imported and "just work".

The PrivateAssets part is to make sure it doesn't appear as a dependency of your project.

What's a metapackage?

It's a package that contains no payload, just depends on a number of other packages, as a convenient way to add a group of packages. You can read more here.

Here is what the metapackage dependencies look like:

... and so on.

I don't want to download all TFMs

As you can see from the screenshot above, the metapackage references all the .NET Framework TFMs (Target Framework Moniker), you wouldn't want to download all of these at build time when you only need one.

Well there's no need to worry, when you do a NuGet restore, dependencies are resolved per framework. So let's say we had this in our project file:

<TargetFrameworks>netstandard2.0;net461</TargetFrameworks>

If we do a NuGet restore, we can look at the contents of /obj/project.asset.json and we will see:

"Microsoft.NETFramework.ReferenceAssemblies/1.0.0": {
"type": "package",
"dependencies": {
"Microsoft.NETFramework.ReferenceAssemblies.net461": "1.0.0"
}
}

Notice how it's only resolved Microsoft.NETFramework.ReferenceAssemblies.net461 as a dependency, and not all of the others.

How does it work?

If we unzip the net461 package and open it up, it looks like this:

The build/.NETFramework/v4.6.1 folder contains the reference assemblies, and the Microsoft.NETFramework.ReferenceAssemblies.net461.targets file looks like this:

<Project>
<PropertyGroup Condition=" ('$(TargetFrameworkIdentifier)' == '.NETFramework') And ('$(TargetFrameworkVersion)' == 'v4.6.1') ">
<TargetFrameworkRootPath>$(MSBuildThisFileDirectory)</TargetFrameworkRootPath>
<!-- FrameworkPathOverride is typically not set to the correct value, and the common targets include mscorlib from FrameworkPathOverride.
So disable FrameworkPathOverride, set NoStdLib to true, and explicitly reference mscorlib here. -->
<EnableFrameworkPathOverride>false</EnableFrameworkPathOverride>
<NoStdLib>true</NoStdLib>
</PropertyGroup>
<ItemGroup Condition=" ('$(TargetFrameworkIdentifier)' == '.NETFramework') And ('$(TargetFrameworkVersion)' == 'v4.6.1') ">
<Reference Include="mscorlib" Pack="false" />
</ItemGroup>

</Project>

Notice that the top level condition means that this package will do nothing when you are not building for net461 (this is evaluated per-tfm for multi-targeting).

EnableFrameworkPathOverride is set to false, so the FrameworkPathOverride we used in the previous post can be removed as it will be ignored anyway, and instead it sets the TargetFrameworkRootPath, which is how it will find the assemblies shipped in the package.

Summary

This package works great and you can take out those old workarounds and custom NuGet feed. This is excellent work from the dotnet team!