This post is chiefly directed at .NET developers and others involved in the various stages of .NET deployment, in particular, anyone that’s been keeping tabs on the situation with the new cross-platform, open-source .NET Core initiative or .NET Standard, which came about as Microsoft’s response to the increased fragmentation of the .NET Platform as a result of the myriad of different deployment targets now available. If you’re not into that kind of stuff, feel free to skip this post, or read on and we’ll try to explain things sufficiently as we go through.
When a new Microsoft, with Satya Nadella at the helm, first open sourced the .NET Platform on November 12, 2014 it became clear that they fully intended to put everything they had into the initiative and that great things and big changes were coming to the .NET Framework and its languages. But what it also signaled was the inevitable beginning of a new level of fragmentation for the Framework, which had thus far – by and large – resisted any major fragmentation for the past 12 years of its existence.1 But taking a framework that was cobbled together from parts old and new, built atop of WIN32, GDI, and various Windows-specific anachronisms meant that porting the .NET Framework as-is to other platforms was nigh-impossible — and that major changes would have to be made to support this gargantuan effort.
Prior to the release of .NET Core (the “cross-platform .NET Framework”), the .NET Framework basically consisted of a number of official releases, each more-or-less a superset of the preceding version. With the exception of the original .NET 1.0 and 1.1 versions of the Framework,2 support from both Microsoft and ISVs for all versions of the platform was more-or-less, officially or not so officially ongoing. Despite some initial confusion about as a result of the munging of the framework version with the runtime version, .NET’s fragmentation was minimal. You simply chose a version of the framework to target; choosing a lower version meant a wider base of support but access to fewer APIs and new-fangled features, while opting to build against a newer version of the framework meant gaining access to newer functionality and awesomeness but an inability to support older operating systems that could only run certain releases of the framework.
As a strict matter of supported APIs, the .NET Framework circa 2014 looked like this:
As you can see, it was all a very neat Venn diagram, each framework featuring all the APIs previous versions supported and then some. Microsoft also did a great job with supporting Windows XP as a first-class .NET target all the way until the release of .NET 4.5 in August of 2012, making it quite easy to target all .NET platforms with a single binary using a relatively-recent version of the framework.3 If you looked a little closer, in reality the picture was more like this, with two different versions of the Common Language Runtime (remember, .NET, despite never getting the JVM’s bad reputation, is a JIT’d platform):
The two different versions of the CLR could be installed side-by-side, but technically version 4 of the CLR did not support running programs compiled against .NET Framework versions .NET 2.0, .NET 3.0, or .NET 3.5, allowing Microsoft to break ABI compatibility to make some needed upgrades to the CLR to support features in .NET 4.0 and above. However, the APIs themselves were backwards compatible enough that one could force a .NET 2.0 binary compiled in late 2005 to run on even .NET 4.7 (not shown above) under v4.0 of the CLR in 2017.45
However, with the introduction of .NET Core, the first version of .NET released under the new cross-platform and open-source initiative, building an application or releasing a library that would support all versions of .NET suddenly became a lot trickier. APIs supported by even versions 1.0 – 4.7 of the “standard” .NET Framework were not supported by .NET Core. While the System.Windows.Forms
UI namespace that was built directly atop of the Win32 GUI bindings was understandably missing from .NET Core – as was even the WPF/XAML-based, non-platform-specific “managed” UI of .NET 3.0+ – many other “core” features of the framework (BCL, or Base Class Library) that developers had been using for a decade+ were also removed in order to support a clean break from the OS-specific underpinnings of the legacy .NET Framework.
To reuse Microsoft’s own illustration, here’s what things suddenly looked like (for the sake of this discussion, it is fine to ignore the 3rd column denoted “Xamarin” which is added to the mix due to Microsoft’s buyout of Xamarin, makers of Mono):
As you can see, along with .NET Core came another UI toolkit, the UWP (Universal Windows Platform). While also not supported on non-Microsoft platforms, UWP is the new UI toolkit of the day for modern Windows platforms, based off of Metra and XAML, and available on Windows 10, Windows 10 Mobile, and the XBox. The biggest problem in the image above, however, is the enormous difference between the supported APIs in the BCL and the Core Library. What Microsoft did was to split off massive chunks of BCL code into separate, smaller libraries while maintaining the absolute minimum, shared code for the .NET ecosystem in the new “.NET Core” library that would be available on all .NET-supporting platforms and ecosystems, across all versions of the “new” framework in years to come.
To make sense of this mess and to mitigate the effects of this massive shakeup and the ensuing fragmentation, in 2016 (two years after the announcement of .NET Core), Microsoft “released” .NET Standard, which can most-effectively be considered a “meta target” that can be used to indicate which subset of the .NET platform (.NET Framework vs .NET Core vs Xamarin) would be compatible with the resulting code. (For those of you that remember .NET Portable and the PCL… well, it’s a lot like that, except we’re pretending PCL doesn’t exist for the sake of having a sane discussion.) To once more use another of Microsoft’s illustrations to show where .NET Standard fits into the mix:
But don’t take the graphic above too literally. As we said, .NET Standard isn’t actually a library or a target, it is just a list of core APIs that are guaranteed to be available across all “conforming” .NET Targets that support that particular version. It is not a unifying update to the .NET Framework, .NET Core, and Xamarin that replaces the BCL, Core Library, and Mono Class Library with a single library, but rather a versioned spec defining a minimum set of APIs that are guaranteed to run with mostly-identical results on all three libraries. Incremental versions of each of the three system libraries would implement more and more common features, culminating in subsequent “releases” of .NET Standard, allowing developers to use a greater number of APIs whilst still targeting all three platforms:
Now that we’re all (hopefully) caught up on the situation with the .NET Framework, .NET Core, and .NET Standard, we can (finally!) get to what we actually wanted to discuss: the unfortunate beginning of a new fragmentation in this fragile, new ecosystem that is .NET Standard.
As you can see from the above chart (circa March, 2017), an application compiled today against the .NET Standard 1.4 metatarget would run on platforms implementing .NET Core 10, .NET 4.6.1, Mono 4.6, Xamarin.iOS 10.0, Xamarin.Android 7.0, and UWP 10.0. Likewise, a library compiled against the .NET Standard 1.4 metatarget can be used in any other application or library also targeting .NET Standard 1.4 (or higher). This is how Microsoft intends to deal with the .NET ecosystem fragmentation: by continuously updating the .NET Standard spec and independently releasing versions of the various .NET implementations for the various supported platforms that support that standard (hence the .NET Standard).
The only problem? For reasons we cannot quite understand, Microsoft has been releasing packages labelled as being compatible with .NET Standard version X, while in reality only supporting a specific subset of the .NET platforms implementing that version of .NET Standard.
As we mentioned before (thanks for bearing with us, btw), when .NET Core was introduced and its new Core Library supplanted the BCL, Microsoft made available huge chunks of the BCL (some of them completely re-implemented for cross-platform support) as packages that supported .NET Core, to provide access to BCL APIs not in the new Core Library. These packages were released on NuGet (which we’ve somehow managed to avoid even mentioning in the above discussion), making it easy to mark dependencies on libraries in your own library/application to make deployment on both .NET Framework and .NET Core platforms easier. These packages are now compiled against versions of the .NET Standard library where supported, making it easy – in theory – to figure out what packages you can and can’t use to support a certain target.
Again, in theory, the formula for figuring out what APIs/packages you can use is quite simple: pick the environments you want your library/application to run in, figure out the greatest common denominator of the supported .NET Standard that these environments implement, and target that, and make sure to only use other packages and dependencies that include support for that version of .NET Standard. In theory.
Here’s what it looks like when you want to add a dependency via NuGet in Visual Studio 2017 (unfortunately there’s currently zero indication in the actual NuGet search results as to whether or not a package is supported by your project’s currently-targeted version of .NET):
As you can see in the lower-half of the right-hand pane, there’s a list of dependencies for the selected package, broken down by target. It basically reads along the lines of “if you’re compiling against version X of .NET, you need dependencies Y and Z,” where “version X of .NET” means .NET Framework v2 – v4.7, .NET Portable, Xamarin, etc. etc. etc. – or just .NET Standard vSomething, as a “shortcut” to indicate “any platform implementing version X of the .NET Standard.” It’s quite effective, and very scalable.
Except here’s what happens when you look at the package for System.Threading.Thread
, which is one of the more-important namespaces in the original BCL that was spun off into its own package when it was stripped from the Core Library:
As you can see, its list of supported platforms includes “.NETStandard,Version=v1.3” – meaning it can be used on any platform implementing version 1.3 of the .NET Standard spec. Referring once more to the .NET Standard chart earlier in this post, we see that this includes .NET Core, .NET 4.6, Mono, Xamarin iOS, Xamarin Android, and UWP 10.0. On some of these platforms, it is likely a no-op, as System.Threading.Thread
is a core namespace already provided by .NET 4.6, Mono, and Xamarin for iOS/Android, without need for an additional package. However, UWP and .NET Core do not have this namespace, and that’s where this package is supposed to come in.
However, this package does not support UWP 10.0! Trying to install this package in a project targeting UWP 10.0 leads to the following error:
PM> Install-Package System.Threading.Thread GET https://api.nuget.org/v3/registration1-gz/system.threading.thread/index.json OK https://api.nuget.org/v3/registration1-gz/system.threading.thread/index.json 12ms Restoring packages for C:\Users\Mahmoud\git\WinMessage\WinMessageUwp\project.json... Install-Package : One or more projects are incompatible with UAP,Version=v10.0. At line:1 char:1 + Install-Package System.Threading.Thread + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [Install-Package], Exception + FullyQualifiedErrorId : NuGetCmdletUnhandledException,NuGet.PackageManagement.PowerShellCmdlets.InstallPackageCommand Install-Package : System.Threading.Thread 4.3.0 provides a compile-time reference assembly for System.Threading.Thread on UAP,Version=v10.0, but there is no run-time assembly compatible with win10-arm. At line:1 char:1 + Install-Package System.Threading.Thread + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [Install-Package], Exception + FullyQualifiedErrorId : NuGetCmdletUnhandledException,NuGet.PackageManagement.PowerShellCmdlets.InstallPackageCommand Install-Package : One or more projects are incompatible with UAP,Version=v10.0 (win10-arm). At line:1 char:1 + Install-Package System.Threading.Thread + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [Install-Package], Exception + FullyQualifiedErrorId : NuGetCmdletUnhandledException,NuGet.PackageManagement.PowerShellCmdlets.InstallPackageCommand Install-Package : One or more packages are incompatible with UAP,Version=v10.0 (win10-arm). At line:1 char:1 + Install-Package System.Threading.Thread + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [Install-Package], Exception + FullyQualifiedErrorId : NuGetCmdletUnhandledException,NuGet.PackageManagement.PowerShellCmdlets.InstallPackageCommand Install-Package : System.Threading.Thread 4.3.0 provides a compile-time reference assembly for System.Threading.Thread on UAP,Version=v10.0, but there is no run-time assembly compatible with win10-arm-aot. At line:1 char:1 + Install-Package System.Threading.Thread + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [Install-Package], Exception + FullyQualifiedErrorId : NuGetCmdletUnhandledException,NuGet.PackageManagement.PowerShellCmdlets.InstallPackageCommand Install-Package : One or more projects are incompatible with UAP,Version=v10.0 (win10-arm-aot). At line:1 char:1 + Install-Package System.Threading.Thread + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [Install-Package], Exception + FullyQualifiedErrorId : NuGetCmdletUnhandledException,NuGet.PackageManagement.PowerShellCmdlets.InstallPackageCommand Install-Package : One or more packages are incompatible with UAP,Version=v10.0 (win10-arm-aot). At line:1 char:1 + Install-Package System.Threading.Thread + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [Install-Package], Exception + FullyQualifiedErrorId : NuGetCmdletUnhandledException,NuGet.PackageManagement.PowerShellCmdlets.InstallPackageCommand Install-Package : System.Threading.Thread 4.3.0 provides a compile-time reference assembly for System.Threading.Thread on UAP,Version=v10.0, but there is no run-time assembly compatible with win10-x64. At line:1 char:1 + Install-Package System.Threading.Thread + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [Install-Package], Exception + FullyQualifiedErrorId : NuGetCmdletUnhandledException,NuGet.PackageManagement.PowerShellCmdlets.InstallPackageCommand Install-Package : One or more projects are incompatible with UAP,Version=v10.0 (win10-x64). At line:1 char:1 + Install-Package System.Threading.Thread + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [Install-Package], Exception + FullyQualifiedErrorId : NuGetCmdletUnhandledException,NuGet.PackageManagement.PowerShellCmdlets.InstallPackageCommand Install-Package : One or more packages are incompatible with UAP,Version=v10.0 (win10-x64). At line:1 char:1 + Install-Package System.Threading.Thread + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [Install-Package], Exception + FullyQualifiedErrorId : NuGetCmdletUnhandledException,NuGet.PackageManagement.PowerShellCmdlets.InstallPackageCommand Install-Package : System.Threading.Thread 4.3.0 provides a compile-time reference assembly for System.Threading.Thread on UAP,Version=v10.0, but there is no run-time assembly compatible with win10-x64-aot. At line:1 char:1 + Install-Package System.Threading.Thread + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [Install-Package], Exception + FullyQualifiedErrorId : NuGetCmdletUnhandledException,NuGet.PackageManagement.PowerShellCmdlets.InstallPackageCommand Install-Package : One or more projects are incompatible with UAP,Version=v10.0 (win10-x64-aot). At line:1 char:1 + Install-Package System.Threading.Thread + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [Install-Package], Exception + FullyQualifiedErrorId : NuGetCmdletUnhandledException,NuGet.PackageManagement.PowerShellCmdlets.InstallPackageCommand Install-Package : One or more packages are incompatible with UAP,Version=v10.0 (win10-x64-aot). At line:1 char:1 + Install-Package System.Threading.Thread + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [Install-Package], Exception + FullyQualifiedErrorId : NuGetCmdletUnhandledException,NuGet.PackageManagement.PowerShellCmdlets.InstallPackageCommand Install-Package : System.Threading.Thread 4.3.0 provides a compile-time reference assembly for System.Threading.Thread on UAP,Version=v10.0, but there is no run-time assembly compatible with win10-x86. At line:1 char:1 + Install-Package System.Threading.Thread + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [Install-Package], Exception + FullyQualifiedErrorId : NuGetCmdletUnhandledException,NuGet.PackageManagement.PowerShellCmdlets.InstallPackageCommand Install-Package : One or more projects are incompatible with UAP,Version=v10.0 (win10-x86). At line:1 char:1 + Install-Package System.Threading.Thread + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [Install-Package], Exception + FullyQualifiedErrorId : NuGetCmdletUnhandledException,NuGet.PackageManagement.PowerShellCmdlets.InstallPackageCommand Install-Package : One or more packages are incompatible with UAP,Version=v10.0 (win10-x86). At line:1 char:1 + Install-Package System.Threading.Thread + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [Install-Package], Exception + FullyQualifiedErrorId : NuGetCmdletUnhandledException,NuGet.PackageManagement.PowerShellCmdlets.InstallPackageCommand Install-Package : System.Threading.Thread 4.3.0 provides a compile-time reference assembly for System.Threading.Thread on UAP,Version=v10.0, but there is no run-time assembly compatible with win10-x86-aot. At line:1 char:1 + Install-Package System.Threading.Thread + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [Install-Package], Exception + FullyQualifiedErrorId : NuGetCmdletUnhandledException,NuGet.PackageManagement.PowerShellCmdlets.InstallPackageCommand Install-Package : One or more projects are incompatible with UAP,Version=v10.0 (win10-x86-aot). At line:1 char:1 + Install-Package System.Threading.Thread + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [Install-Package], Exception + FullyQualifiedErrorId : NuGetCmdletUnhandledException,NuGet.PackageManagement.PowerShellCmdlets.InstallPackageCommand Install-Package : One or more packages are incompatible with UAP,Version=v10.0 (win10-x86-aot). At line:1 char:1 + Install-Package System.Threading.Thread + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [Install-Package], Exception + FullyQualifiedErrorId : NuGetCmdletUnhandledException,NuGet.PackageManagement.PowerShellCmdlets.InstallPackageCommand Install-Package : Package restore failed. Rolling back package changes for 'WinMessageUwp'. At line:1 char:1 + Install-Package System.Threading.Thread + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [Install-Package], Exception + FullyQualifiedErrorId : NuGetCmdletUnhandledException,NuGet.PackageManagement.PowerShellCmdlets.InstallPackageCommand
While Microsoft’s System.Threading.Thread
package purports to support .NET Standard 1.3, it provides no implementation compatible with UWP targets! What’s worse is that there’s zero indication from without that this package does not actually support .NET Standard 1.3 like it claims it does – meaning that there are literally hundreds, if not thousands, of popular packages listed on NuGet (many of which are also developed by Microsoft) that are developed with universal support for .NET Standard 1.3 implementations – yes, including UWP – that will never actually work on UWP targets because they use the System.Threading.Thread package to provide thread support.
This is not an unknown issue or an oversight. It’s been discussed to death on GitHub, and it’s due to be implemented for .NET Standard 2.0 (despite some people actually being oddly outraged at the suggestion) but that does nothing to address the fact that Microsoft is knowingly publishing packages that claim to support .NET Standard when they in fact don’t (bugged here), that there are thousands of packages out there that think they can be used on Windows Mobile and in UWP 10 applications for the Windows 10 store but they, in fact, can’t.
The saddest part of it is that it is really easy for Microsoft to have provided this package for .NET Core but not .NET Standard (it’s just a nuspec edit away!), in which case all this would have been avoided. As it currently stands, this is one package that’s insisting on re-introducing fragmentation to the .NET ecosystem, .NET Standard be damned.
All complaining aside, we’ve published our own, mostly complete implementation of System.Threading.Thread
built on the System.Threading.Tasks
namespace for .NET Standard 1.3 (yes, including UWP) on NuGet and open-sourced it on GitHub. We’ve also contributed fixes to a number of popular open source projects available via NuGet that ostensibly supported .NET Standard and UWP but in fact didn’t due to dependencies on System.Threading.Thread
.
Believe it or not, this post came from a place of love and out of a desperate urge to voice our concerns in hopes of avoiding further fragmentation of a fragile ecosystem.
There are notable exceptions to this, namely the .NET Micro Framework, the .NET Compact Framework, and Mono; however, these “members” of the .NET ecosystem – one not even by Microsoft – were never considered to be first-class .NET Targets within or without Microsoft. ↩
It is generally accepted as taboo to speak of the existence of these versions of the .NET Framework; even developers like myself that learned .NET via (wait for it!) J# (<em>gasp!</em>) on .NET 1.0 pretend that .NET 2.0 is where it all really started. ↩
There’s a caveat, of course. .NET 2.0 – .NET 3.5 are technically not guaranteed ABI-compatible with .NET 4.0+ due to the CLR change (v2.0 vs v4.0), but developers – including NeoSmart Technologies – have long-targeted both CLR 2.0 and CLR 4.0 with a single binary via some manifest file sorcery. ↩
Additional catches included the fact that .NET 3.0 shipped out-of-the-box with Windows Vista, meaning .NET 2.0 and .NET 3.0 applications would be guaranteed to run on Windows Vista without the installation of any additional dependencies or frameworks (and .NET 3.5.1 came with Windows 7, meaning all CLR 2.0 applications could be run on Windows 7 out-of-the-box), but Windows 8 and Windows 10 shipped with .NET 4.5 and .NET 4.6 respectively, meaning the .NET 3.5 framework had to be installed on these platforms prior to running .NET 2.0 – .NET 3.5 applications, which made deployment a little harder. Applications like our own EasyBCD, iReboot, and Easy USB Creator abuse the “supportedRuntime” feature to force all versions of the CLR to run them out-of-the-box, however. ↩
.NET 4.0 also enabled an optional fragmentation where a smaller version of the runtime without ASP.NET and other server-specific code known as the “client profile” could be installed, but the fact that by default .NET 4.0 meant the entire 4.0 makes it easy to leave that particular wrench out of this discussion. ↩
Very informative article