blog post image
Andrew Lock avatar

Andrew Lock

~16 min read

Understanding the .NET ecosystem: The introduction of .NET Standard

This post is the second of a two part series on understanding the .NET ecosystem. I've been working hard on the latest edition of my book, ASP.NET Core in Action, Third Edition, and there's a lot of new content in this edition. We chose to remove one of the appendices from the book, trying to make sense of the .NET ecosystem. Rather than throw it away, I've turned it into a couple of posts here!

Although this post didn't make it into the book, if you like what you see, take a look at ASP.NET Core in Action—for now you can even get a 35% discount by entering the code au35loc into the discount code box at checkout at manning.com. On top of that, you'll also get ebook copies of the first and second editions, free!

The previous post covers:

  • The history of .NET, leading to the development of .NET Core
  • The position of .NET 7 in the .NET ecosystem

In this post I cover:

  • Sharing code between projects with .NET Standard
  • The future of .NET Standard with .NET 7

In this post I’ll discuss the history of sharing code between .NET platforms using Portable Class Libraries. I’ll then introduce .NET Standard as an alternative solution that was introduced with .NET Core.

With so many different .NET implementations, the .NET ecosystem needed a way to share code between libraries, long before .NET Core was envisaged. What if you wanted to use the same classes in both your ASP.NET .NET Framework project and your Silverlight project? You’d have to create a separate project for each platform, copy and paste files between them, and recompile your code for each platform. The result was two different libraries from the same code. Portable Class Libraries (PCLs) were the initial solution to this problem.

Finding a common intersection with Portable Class Libraries

PCLs were introduced to make the process of compiling and sharing code between multiple platforms simpler. When creating a library, developers could specify the platforms they wanted to support, and the project would only have access to the set of APIs common among all of them. Each additional platform supported would reduce the API surface to only those APIs available in all the selected platforms, as shown in figure 4.

Image showing that each additional framework that must be supported by a PCL reduces the APIs available to your application.
Figure 4. Each additional framework that must be supported by a PCL reduces the APIs available to your application. If you support multiple frameworks, you have vastly fewer APIs available to you.

To create a PCL library, you’d create a library that targeted a specific PCL “profile.” This profile contained a precomputed list of APIs known to be available on the associated platforms. That way, you could create one library that you could share across your selected platforms. You could have a single project and a single resulting package—no copy and paste or duplicate projects required.

This approach was a definite improvement over the previous option, but creating PCLs was often tricky. There were inherent tooling complexities to contend with, and understanding the APIs available for each different combination of platforms that made up a PCL profile was difficult.

Tip See here for the full horrifying list: https://portablelibraryprofiles.stephencleary.com/.

On top of these issues, every additional platform you targeted would reduce the BCL API surface available for you to use in your library. For example, the .NET Framework might contain APIs A, B, and C. But if Xamarin only has API A and Universal Windows Platform (UWP) only has API C, your library can’t use any of them, as shown in figure 5.

Image showing that each platform exposes slightly different APIs. When creating PCLs, only those APIs that are available in all the targeted platforms are available.
Figure 5. Each platform exposes slightly different APIs. When creating PCLs, only those APIs that are available in all the targeted platforms are available. In this case, none of the APIs, A, B, or C, is available in all targeted platforms, so none of them can be used in the PCL.

One additional issue with PCL libraries was that they were inherently tied to the underlying platforms they targeted. In order to work with a new target platform, you’d have to recompile the PCL, even if no source code changes were required.

Say you’re using a PCL library that supports UWP 10.0 and .NET Framework 4.5. If Microsoft were to release a new platform, let’s say .NET Fridge, which exposes the same API as UWP 10.0, you wouldn’t be able to use the existing library with your new .NET Fridge application. Instead, you’d have to wait for the library author to recompile their PCL to support the new platform, and who knows when that would be!

PCLs had their day, and they solved a definite problem, but for modern development .NET Standard provides a much cleaner approach.

.NET Standard: A common interface for .NET

As part of the development of .NET Core, Microsoft announced .NET Standard as the successor to PCL libraries. .NET Standard takes the PCL relationship between platform support and APIs available, and flips it on its head:

  • PCLs—A PCL profile targets a specific set of platforms. The APIs available to a PCL library are the common APIs shared by all the platforms in the profile.
  • .NET Standard—A .NET Standard version defines a specific set of APIs. These APIs are always available in a .NET Standard library. Any platform that implements all these APIs supports that version of .NET Standard.

.NET Standard isn’t something you download. Instead, it’s a list of APIs that a .NET Standard-compatible platform must implement. You can create libraries that target .NET Standard, and you can use that library in any app that targets a .NET Standard-compatible platform.

Note .NET Standard is, literally, a list of APIs. You can view the APIs included in each version of .NET Standard on GitHub. For example, you can see the APIs included in .NET Standard 1.0 here: https://github.com/dotnet/standard/blob/release/3.0/docs/versions/netstandard1.0.md.

.NET Standard has multiple versions, each of which is a superset of the previous versions. For example, .NET Standard 1.2 includes all the APIs from .NET Standard 1.1, which in turn includes all the APIs from .NET Standard 1.0, as shown in figure 6.

Image showing that each version of .NET Standard includes all the APIs from previous versions. The smaller the version of .NET Standard, the smaller the number of APIs.
Figure 6. Each version of .NET Standard includes all the APIs from previous versions. The smaller the version of .NET Standard, the smaller the number of APIs.

When you create a .NET Standard library, you target a specific version of .NET Standard and can reference any library that targets that version or earlier. If you’re writing a library that targets .NET Standard 1.2, you can reference packages that target .NET Standard 1.2, 1.1, or 1.0. Your package can, in turn, be referenced by any library that targets .NET Standard 1.2 or later, or any library that targets a platform that implements .NET Standard 1.2 or later.

A platform implements a specific version of .NET Standard if it contains all the APIs required by that version of .NET Standard. By extension, a platform that supports a specific version of .NET Standard also supports all previous versions of .NET Standard. For example, UWP version 10 supports .NET Standard 1.4, which means it also supports .NET Standard versions 1.0–1.3, as shown in figure 7.

Image showing the nested nature of .NET Standard APIs.
Figure 7. The UWP Platform version 10 supports .NET Standard 1.4. That means it contains all the APIs required by the .NET Standard specification version 1.4, which means it also contains all the APIs in earlier versions of .NET Standard. It also contains additional platform-specific APIs that aren’t part of any version of .NET Standard.

Each version of a platform supports a different version of .NET Standard. .NET Framework 4.5 supports .NET Standard 1.1, but .NET Framework 4.7.1 supports .NET Standard 2.0. Table 1 shows some of the versions supported by various .NET platforms. For a more complete list, see Microsoft’s “.NET Standard” overview: https://learn.microsoft.com/dotnet/standard/net-standard.

Table 1. Highest supported .NET Standard version for various .NET platform versions. A blank cell means that version of .NET Standard isn’t supported on the platform.
.NET Standard version11.11.21.31.41.51.622.1
.NET Core and .NET111111123
.NET Framework4.54.54.5.14.64.6.14.6.24.7.1
Mono4.64.64.64.64.64.64.65.46.4
UWP101010101010.0.1629910.0.1629910.0.16299
Windows888.1
Windows Phone8.18.18.1

A version of this table is often used to explain .NET Standard, but for me the relationship between .NET Standard and a .NET platform all made sense when I saw an example that explained .NET Standard in terms of C# constructs.

Tip The original example was provided by David Fowler from the ASP.NET team. You can view an updated version of this metaphor here: https://github.com/dotnet/standard/blob/release/3.0/docs/metaphor.md.

You can think of each version of .NET Standard as a series of inherited interfaces, and the .NET platforms as implementations of one of these interfaces. In the following listing, I use the last two rows of table 1 to illustrate this, considering .NET Standard 1.0–1.2 and looking at the Windows 8.0 platform and Windows Phone 8.1.

// Defines the APIs available in .NET Standard 1.0
interface NETStandard1_0 
{
   void SomeMethod();
} 

// .NET Standard 1.1 inherits all the APIs from .NET Standard 1.0.
interface NETStandard1_1 : NETStandard1_0 
{
    // APIs available in .NET Standard 1.1 but not in 1.0
    void OtherMethod();
}

// .NET Standard 1.2 inherits all the APIs from .NET Standard 1.1.
interface NETStandard1_2 : NETStandard1_1
{
    // APIs available in .NET Standard 1.2 but not in 1.1 or 1.0
    void YetAnotherMethod();
}

// Windows 8.0 implements .NET Standard 1.1.
class Windows8 : NETStandard1_1
{
    // Implementations of the APIs required by .NET Standard 1.1 and 1.0
    void SomeMethod () { /* Method implementation */ }
    void OtherMethod() { /* Method implementation */ }

    // Additional APIs that aren’t part of .NET Standard, but exist on the Windows 8.0 platform
    void ADifferentMethod() { /* Method implementation */ }
}

// Windows Phone 8.1 implements .NET Standard 1.2.
class WindowsPhone81 : NETStandard1_2
{
    // Implementations of the APIs required by .NET Standard 1.2, 1.1, and 1.0
    void SomeMethod () { /* Method implementation */ }
    void OtherMethod() { /* Method implementation */ }
    void YetAnotherMethod () { /* Method implementation */ }

    // Additional APIs that aren’t part of .NET Standard, but exist on the Windows Phone 8.1 platform
    void ExtraMethod1() { /* Method implementation */ }
    void ExtraMethod2() { /* Method implementation */ }
}

In the same way that you write programs to use interfaces rather than specific implementations, you can target your libraries against a .NET Standard interface without worrying about the individual implementation details of the platform. You can then use your library with any platform that implements the required interface version.

One of the advantages you gain by targeting .NET Standard is the ability to target new platforms without having to recompile any of your libraries or wait for dependent library authors to recompile theirs. It also makes reasoning about the exact APIs available far simpler—the higher the .NET Standard version you target, the more APIs will be available to you.

Warning Even if a platform implements a given version of .NET Standard, the method implementation might throw a PlatformNotSupportedException. For example, some reflection APIs might not be available on all platforms. .NET 7 includes Roslyn Analyzer support to detect this situation and will warn you of the issue at build time. See “Automatically find latent bugs in your code with .NET 5” on the .NET Blog (http://mng.bz/WdX4) for more details about the analyzers originally introduced in .NET 5.

Unfortunately, things are never as simple as you want them to be. Although .NET Standard 2.0 is a strict superset of .NET Standard 1.6, apps targeting .NET Framework 4.6.1 can reference .NET Standard 2.0 libraries, even though it technically only implements .NET Standard 1.4, as shown previously in table 1.

Warning Even though .NET Framework 4.6.1 technically only implements .NET Standard 1.4, it can reference .NET Standard 2.0 libraries. This is a special case and applies only to versions 4.6.1–4.7.0. .NET Framework 4.7.1 implements .NET Standard 2.0, so it can reference .NET Standard 2.0 libraries natively.

The reasoning behind this move was to counteract a chicken-and-egg problem. One of the early complaints about .NET Core 1.x was how few APIs were available, which made porting projects to .NET Core tricky. Consequently, in .NET Core 2.0, Microsoft added thousands of APIs that were available in .NET Framework 4.6.1, the most widely installed .NET Framework version, and added these APIs to .NET Standard 2.0. The intention was for .NET Standard 2.0 to provide the same APIs as .NET Framework 4.6.1.

Tip The reasoning behind this move was laid out in the “Introducing .NET Standard” post on the .NET Blog, which I highly recommend reading: https://devblogs.microsoft.com/dotnet/introducing-net-standard/.

Unfortunately, .NET Framework 4.6.1 doesn’t contain the APIs in .NET Standard 1.5 or 1.6. Given that .NET Standard 2.0 is a strict superset of .NET Standard 1.6, .NET Framework 4.6.1 can’t support .NET Standard 2.0 directly.

This left Microsoft with a problem. If the most popular version of the .NET Framework didn’t support .NET Standard 2.0, no one would write .NET Standard 2.0 libraries, which would hamstring .NET Core 2.0 as well. Consequently, Microsoft took the decision to allow .NET Framework 4.6.1 to reference .NET Standard 2.0 libraries, as shown in figure 8.

Image showing that .NET Framework 4.6.1 doesn't include all the necessary ASPIs to support .NET Standard 1.5+.
Figure 8. .NET Framework 4.6.1 doesn’t contain the APIs required for .NET Standard 1.5, 1.6, or 2.0. But it contains nearly all the APIs required for .NET Standard 2.0. In order to speed up adoption and to make it easier to start using .NET Standard 2.0 libraries, you can reference .NET Standard 2.0 libraries for a .NET Framework 4.6.1 app.

All this leads to some fundamental technical difficulties. .NET Framework 4.6.1 can reference .NET Standard 2.0 libraries, even though it technically doesn’t support them, but you must have the .NET Core 2.0 SDK installed to ensure everything works correctly.

To blur things even further, libraries compiled against .NET Framework 4.6.1 can be referenced by .NET Standard libraries through the use of a compatibility shim, as I’ll describe in the next section.

Fudging .NET Standard 2.0 support with the compatibility shim

Microsoft’s plan for .NET Standard 2.0 was to make it easier to build .NET Core apps. If users built libraries targeting .NET Standard 2.0, they could still use them in their .NET Framework 4.6.1 apps, but they could also use the libraries in their .NET Core apps.

The problem was that when .NET Standard 2.0 was first released, no libraries (NuGet packages) would implement it yet. Given that .NET Standard libraries can only reference other .NET Standard libraries, you’d have to wait for all your dependencies to update to .NET Standard, which would have to wait for their dependencies first, and so on.

To speed things up, Microsoft created a compatibility shim. This shim allows a .NET Standard 2.0 library to reference .NET Framework 4.6.1 libraries. Ordinarily, this sort of reference wouldn’t be possible; .NET Standard libraries can only reference .NET Standard libraries of an equal or lower version, as shown in figure 9.

Note The process by which this magic is achieved is complicated. This “.NET Standard 2” article on GitHub describes the process of assembly unification in detail: https://github.com/dotnet/standard/blob/release/3.0/docs/planning/netstandard-2.0/README.md.

Image showing By default, .NET Standard libraries can only reference other .NET Standard libraries, targeting the same .NET Standard version or lower. With the compatibility shim, .NET Standard libraries can also reference libraries compiled against .NET Framework 4.6.1.
Figure 9. By default, .NET Standard libraries can only reference other .NET Standard libraries, targeting the same .NET Standard version or lower. With the compatibility shim, .NET Standard libraries can also reference libraries compiled against .NET Framework 4.6.1.

By enabling this shim, suddenly .NET Core 2.0 apps could use any of the many .NET Framework 4.6.1 (or lower) NuGet libraries available. As long as the referenced library stuck to APIs that are part of .NET Standard 2.0, you’d be able to reference .NET Framework libraries in your .NET Core 2+ apps or .NET Standard 2.0 libraries, even if your app runs cross-platform on Linux or macOS.

Warning If the library uses .NET Framework-specific APIs, you’ll get an exception at runtime. There’s no easy way of knowing whether a library is safe to use, short of examining the source code, so the .NET tooling will raise a warning every time you build. Be sure to thoroughly test your app if you rely on this shim.

If your head is spinning at this point, I don’t blame you. This was a particularly confusing point in the evolution of .NET Standard, in which rules were being bent to fit the current environment. This inevitably led to various caveats and hand-waving, followed by bugs and fixes.

Note You can find an example of one such issue here, but there were, unfortunately, many similar cases: https://github.com/dotnet/runtime/issues/29314.

Luckily, if your development is focused on .NET 7, .NET Standard is not something you will generally have to worry about.

.NET 7 and the future of .NET Standard

In this section I discuss what .NET 7 means for the future of .NET Standard and the approach you should take for new applications targeting .NET 7.

Note The advice in this section is based on the official guidance from Microsoft regarding the future of .NET Standard here http://mng.bz/goqG and here https://learn.microsoft.com/dotnet/standard/net-standard.

.NET Standard was necessary when .NET Core was a young framework, to ensure you still had access to the existing NuGet package ecosystem. .NET 7 is an evolution of .NET Core, so you can take advantage of that same ecosystem in .NET 7.

.NET 7 implements .NET Standard 2.1, the latest version of the standard, which is also implemented by .NET Core 3.0, .NET 5, and .NET 6. That means .NET 7 applications can reference

  • Any NuGet package or library that implements .NET Standard 1.0–2.1
  • Any NuGet package or library that implements .NET Core 1.x–3.x or .NET 5-7

.NET Standard was designed to handle code-sharing between multiple .NET platforms. But the release of .NET 7, and the completion of the One .NET vision, specifically aims to have only a single platform. Is .NET Standard still useful?

Yes and no. From .NET 5 onwards, no more versions of .NET Standard are planned, as subsequent versions of .NET (for example, .NET 7.0) are already able to reference libraries targeting earlier versions of .NET (such as .NET 5 and .NET 6). If you’re creating components to use in apps, target a version of .NET, not .NET Standard. Similarly, if you’re creating libraries that will only be used in modern .NET applications, target .NET, not .NET Standard.

.NET Standard only remains useful when you need to share code between .NET 5+ applications and legacy (.NET Core, .NET Framework, Xamarin) applications. .NET Standard remains the mechanism for this cross-.NET platform code-sharing, as described in this documentation: https://learn.microsoft.com/dotnet/standard/net-standard#net-standard-not-deprecated.

Summary

  • Portable Class Libraries (PCLs) attempted to solve the problem of sharing code between .NET platforms by allowing you to write code to the logical intersection of each platform’s BCL. Each additional platform you targeted meant fewer BCL APIs in common.
  • .NET Standard defines a standard set of APIs that are available across all platforms that support it. You can write libraries that target a specific version of .NET Standard and they’ll be compatible with any platform that supports that version of .NET Standard.
  • Each version of .NET Standard is a superset of the previous one. For example, .NET Standard 1.2 includes all the APIs from .NET Standard 1.1, which in turn includes all the APIs from .NET Standard 1.0.
  • Each version of a platform supports a specific version of .NET Standard. For example, .NET Framework 4.5.1 supports .NET Standard 1.2 (and hence also .NET Standard 1.1 and 1.0).
  • .NET Framework 4.6.1 technically only supports .NET Standard 1.4. Thanks to a compatibility shim, you can reference .NET Standard 2.0 libraries from a .NET Framework 4.6.1 app. Similarly, you can reference a .NET Framework library from a .NET Standard 2.0 library, which wouldn’t be possible without the shim.
  • If you rely on the compatibility shim to reference a .NET Framework 4.6.1 library from a .NET Standard 2.0 library, and the referenced library uses .NET Framework-specific APIs, you’ll get an exception at runtime.
  • An app must target a .NET platform implementation, such as .NET 7 or .NET Core 3.1. It can’t target .NET Standard.
  • .NET 7 supports .NET Standard 2.1. It can reference any .NET Standard library and any .NET Core or .NET 5+ library.

A reminder that this content originally came from the latest edition of my book, ASP.NET Core in Action, Third Edition. Get a 35% discount by entering the code au35loc into the discount code box at checkout at manning.com. You'll also get ebook copies of the first and second editions, free!

Andrew Lock | .Net Escapades
Want an email when
there's new posts?