A little journey under the hood of .NET Standard implementation
If you deal with Microsoft technologies you’ve probably heard about .NET Standard, if not you can think of it as a sort of Portable Class Libraries (PCL)but without the intrinsic limitations that PCL always had.
By using .NET Standard you can create a library that can run unmodified not only inside different contexts (Desktop, Mobile…) but also on different platforms (e.g. on LINUX, OSx…) this thanks to .NET Core and Mono.
The question now is: How is it possible? Does .NET Standard is able to generate code that is able to run on different platforms?
Of course, not. The cross platform story is kindly provided by .NET Core, Mono and .NET Framework in case of desktop apps like Winform or WPF (at the moment, support for .NET Core 3.0 is on the way…)
Ok, but what is .NET Standard and what is the magic that let us share the same code between an ASP.NET Core app running on LINUX and an old fashioned Winform app running maybe on Windows 7?
What .NET Standard is
.NET Standard is nothing but a specification (think of it as an Interface), it just only declares what types and APIs are exposed by a specific platform depending on is version.
Version 1.0 was announced on Sept 26th, 2016 and was limited in terms of supported APIs, it evolved with time and the arrival of version 2.0 increased the number of available APIs to more than 30000 filling the gap with classic libraries projects.
This table (all but clear i know…) shows what kind of implementation of the .NET Standard contracts each platform supports
This means, that, for example, if you want to use a .NET Standard 2.0 library in a UWP app, the minimum target must be Fall Creators Update (10.0.16299) otherwise we can use only the set of APIs exposed by version 1.4.
How things work?
To demonstrate how things works under the hood I’ve created a Visual Studio solution made up of a .NET Standard 2.0 library (SmartCalculator) a .NET Core console app (SmartConsoleApp) and a Winform (!) app (SmartWinforms)
The code is available on GitHub if you want to play with it.
The code inside SmartCalculator library is very simple, since is not the goal of this post
public class SmartCalculator
public int Sum(int a, int b)
return a + b;
public int DaysToChristmas()
var christmas = new DateTime(2018, 12, 25);
var now = DateTime.Now;
return (int)(christmas - now).TotalDays;
Both Console and WinForms apps reference the .NET Standard 2.0 app that, as you see from previous picture, it references the .NET Standard 2.0.3 SDK that it contains a netstandard.dll containing the APIs or, to better say, the contract, we can use from within the library project.
If we run both projects we will see that they’re both able to use our (super complicated!) calculator.
So, we now have a console app running on .NET Core 2.1 and a Windows Forms running on .NET Framework 4.6.1 both consuming a type coming from a ‘shared’ library.
How is it possible?
Let’s dig into details by inspecting what’s inside the SmartCalculator.dll generated by the .NET Standard project using ILDASM.exe tool.
Here’s what’s inside:
Let’s now see what inside class definition and DaysToChristmas method
As you see, all involved types (System.Object, System.DateTime…) are provided by .NET Standard dll, but this leads to another confusion, see the picture below.
ILDasm is telling us that SmartCalculator class, consumed by both NetCore app and Winforms app inherits from a type that resides inside netstandard.dll while we all know that, for NetCore apps, System.Object is provided by System.Runtime.dll while in case of Winform the type is provided by mscorlib.dll.
So, how can this mess work?
Let’s see what’s inside SmartConsoleCalc app Debug folder…
As you see, no trace of netstandard.dll… so how the runtime resolves it?
Let’s open this folder on local hard drive C:\Program Files\dotnet\shared\Microsoft.NETCore.App\ you will find a list of folder, each one representing the versions of .NET Core installed, in my case, at the moment, latest version is 2.1.5.
If you browse inside, together with all the libraries part of .NET Core, you will find netstandard.dll, so now at least we now know where the library is loaded from.
Let’s see now what’s inside that copy of netstandard.dll using ILDASM and we’ll get a bit surprised, because it is totally empty.
Let’s now explore the manifest looking for System.Object definition…
And, here’s the ‘trick’ that makes things works!
As you see System.Object definition inside netstandard.dll just uses type forwarding to redirect type definition to the right dll, that in case of a NetCore app is System.Runtime.dll so that type definition matches when SmartCalculator library is loaded inside a Net Core app.
Let’s do the same with Winforms app that is based on .NET Framework 4.6.1, let’s try to find the same netstandard.dll inside .NET Framework 4.6.1 installation, but…
No trace of netstandard.dll… why? let’s try with latest version 4.7.1, and there you go, inside Facades folder there’s netstandard.dll
Let’s inspect its contents looking for System.Object definition
Bingo!, netstandard.dll forwards System.Object definition to mscorlib.dll and here’s how types are matched also in this case.
What about .NET Framework 4.6.1?
We saw that .NET Framework 4.6.1 doesn’t provide any netstandard.dll, how can things works in this case?
If you look at initial compatibility version table you will see that the .NET Standard version that the full .NET Framework supports changes depending if on your machine is installed just .NET Core 1.0 SDK or also version 2.0 is present.
In second case the .NET Framework team decided to provide .NET Standard 2.0 support also to app running on 4.6.1 via a special trick: the netstandard.dll is automatically copied into output directory.
Here’s output folder when Winforms application is compiled against version 4.6.1 of the framework
and here’s the same output when target framework is upgraded to 4.7.1
In this case, netstandard.dll is not present since it is provided by .NET Framework 4.7.1 installation.
When you create a .NET Standard dll, you compile against a definition set, that changes depending on the version you want to target. The lower, the wider are the context where your dll can be consumed, but the price to pay is that the number of APIs decreases.
Each platform your lib will run to, will take care of providing a netstandard.dll (or all required dlls in case of version < 2.0) that just forwards definition to the right framework dll, making the process totally transparent.
I think version 2.0 onward totally fills the APIs gap and can be considered the official way to go when you want to create any new code library