blog post image
Andrew Lock avatar

Andrew Lock

~15 min read

Converting a .NET Standard 2.0 library to .NET Core 3.0

Upgrading to ASP.NET Core 3.0 - Part 1

This is the first post in a new series on upgrading from ASP.NET Core 2.x to ASP.NET Core 3.0. I'm not going to cover big topics like adding Blazor or gRPC to your apps. Instead I'm going to cover the little confusing things like how to upgrade your libraries to target ASP.NET Core 3.0, switching to use the new generic-host-based server, and using endpoint routing.

If you're starting on an upgrade from ASP.NET Core 2.x to 3.0, I strongly suggest following through the migration guide, reading my series on exploring ASP.NET Core 3.0, and checking out Rick Strahl's post on converting an app to ASP.NET Core 3.0. A recent ASP.NET community standup also walked though the bare minimum for upgrading to 3.0. That should give you a good idea of issues you're likely to run into.

In this post I describe some of the steps and issues I ran into when converting .NET Standard 2.0 class libraries to .NET Core 3.0. I'm specifically looking at converting libraries in this post.

For the purposes of this post, I'll assume you have one or more class libraries that you're in control of, and are trying to decide how to support .NET Core 3.0. I consider the following cases, separated based on your library's dependencies:

Upgrading a .NET Standard 2.0 library to .NET Core 3 - is it necessary?

The first question you have to answer is whether you even need to update your library. Unfortunately, there isn't a simple answer to this question due to some of the changes that came with .NET Core 3.0.

Specifically, .NET Core 3.0 introduces the concept of a FrameworkReference. This is similar to the Microsoft.AspNetCore.App metapackage in ASP.NET Core 2.x apps, but instead of being a NuGet package that references other NuGet packages, the framework is installed along with the .NET Core runtime.

This has implications when your class library references packages that used to exist as NuGet packages, but are now pre-installed as part of the shared framework. I'll try to work through the various combinations of target frameworks and NuGet references your library has, to give you an idea of your options around upgrading your library to work with .NET Core 3.0.

Code-only libraries

Lets start with the simplest case - you have a library that has no other dependencies.

Q: My library targets .NET Standard 2.0 only, and has no dependencies on other NuGet packages

In theory, you shouldn't need to change your library at all. .NET Core 3.0 supports .NET Standard 2.1, and by extension, it supports .NET Standard 2.0.

By continuing to target .NET Standard 2.0, you will be able to consume it in .NET Core 3.0 applications, but you'll also continue to be able to consume it in .NET Core 2.x applications, .NET Framework 4.6.1+ applications, and Xamarin apps, among others.

Q: Should I update my library to target .NET Standard 2.1?

By targeting .NET Standard 2.0, you're allowing a large number of frameworks to consume your library. Upgrading to .NET Standard 2.1 will limit that significantly. You'll no longer be able to consume the library in .NET Core 2.x, .NET Framework, Unity, or earlier Mono/Xamarin versions. So no, you shouldn't target .NET Standard 2.1 just because it's there.

That said, .NET Standard 2.1 includes a number of performance-related primitives that you may want to use in your application, as well as features such as IAsyncEnumerable<>. In order to keep the widest audience, you may want to multi-target both 2.0 and 2.1, and use conditional compilation to take advantage of the primitives on platforms that support them. If you're itching to make use of these new features, or you know your library is only going to be used on platforms that support .NET Standard 2.1 then go ahead. It should be as simple as updating the <TargetFramework> element in your .csproj file.

<Project Sdk="Microsoft.NET.Sdk">
  
  <PropertyGroup>
    <TargetFramework>netstandard2.1</TargetFramework>
    <LangVersion>8.0</LangVersion>
  </PropertyGroup>
</Project>

If you're upgrading to target .NET Standard 2.1 then you may as well update to use C# 8 features. .NET Framework won't support them, but as it doesn't support .NET Standard 2.1 either, that ship has already sailed!

Q: My library targets .NET Core 2.x, and has no dependencies on other NuGet packages

This scenario is essentially the same situation as the previous one. .NET Core 3.0 apps can consume any library that targets .NET Core 3.0 or below, so there's no need to update your library unless you want to. If you're targeting .NET Core 2.x you can use all the features available to the platform (which is more than is in .NET Standard 2.0). If you upgrade to .NET Core 3.0 then you obviously get access to more features again, but you won't be able to consume your library in .NET Core 2.x apps any more.

Q: My library has dependencies on other NuGet packages

Libraries with no dependencies are the easiest to deal with - generally you target the lowest version of .NET Standard you can that gives you all the features you need, and leave it at that. Things get a bit trickier when you have dependencies on other NuGet packages.

However, if your dependencies (and none of your dependencies dependencies, also known as "transitive" dependencies) are not Microsoft.AspNetCore.* or Microsoft.Extensions.* libraries then there's not much to worry about. As long as they support the framework you're trying to target, then you don't need to worry. If you are depending on the Microsoft libraries, then things are more nuanced.

Libraries that depend on Microsoft.Extensions.* NuGet packages

This is where things start to get interesting. The Microsoft.Extensions.* libraries provide generic features such as dependency injection, configuration, logging, and the generic host. Those features are all used by ASP.NET Core apps, but you can also use them without ASP.NET Core for creating all sorts of other services and console apps.

The nice thing about the Microsoft.Extensions.* libraries is they allow you to create libraries that easily hook into the .NET Core ecosystem, making it pretty simple for users to consume your libraries.

In .NET Core 3.0, the Microsoft.Extensions.* libraries all received a major version bump to 3.0.0. They also now multi-target netstandard2.0 and netcoreapp3.0. This poses an interesting question that Brad Wilson recently asked on Twitter:

In other words: Given that .NET Core 2.x apps support .NET Standard 2.0, can you use 3.0.0 Microsoft.Extensions.* libraries in .NET Core 2.x?

Yes! If you're building a console app and are still targeting .NET Core 2.x, you can, if you wish, upgrade your Microsoft.Extension.* library references to 3.0.0. Your app will still work, and you can use the latest abstractions.

OK, what if it's not just a .NET Core app, it's an ASP.NET Core 2.x app?

Well yes, but actually no

The problem is that while you can add a reference to the 3.0.0 library, in ASP.NET Core 2.x apps the core libraries also depend on the Microsoft.Extensions.* libraries. When you try and build your app you'll get an error something like the following:

C:\repos\test\test.csproj : warning NU1608: Detected package version outside of dependency constraint: Microsoft.AspNetCore.App 2.1.1 requires Microsoft.Extensions.Configuration.Abstractions (>= 2.1.1 && < 2.2.0) but version Microsoft.Extensions.Configuration.Abstractions 3.0.0 was 
resolved.
C:\repos\test.csproj : error NU1107: Version conflict detected for Microsoft.Extensions.Primitives. Install/reference Microsoft.Extensions.Primitives 3.0.0 directly to project PwnedPasswords.Sample to resolve this issue.

Trying to solve this issues is a fool's errand. Just accept that you can't use 3.0.0 extension libraries in ASP.NET Core 2.x apps.

Now lets consider the implications to your libraries that depend on the Microsoft.Extensions libraries.

Q: My library uses Microsoft.Extensions.* and will only be used in .NET Core 3.0 apps

If you're building an internal library then you may able to specify that a library is only supported on .NET Core 3.0. In that case, it makes sense to target the 3.0.0 libraries.

Q: My library uses Microsoft.Extensions.* and may be used in both .NET Core 2.x and .NET Core 3.0 apps

This is where things get interesting. In most cases, there's very few differences between the 2.x and 3.0 versions of the Microsoft.Extensions.* libraries. This is especially true if you're using one of the *.Abstractions libraries, such as Microsoft.Extensions.Configuration.Abstractions.

For example for Microsoft.Extensions.Configuration.Abstractions, between versions 2.2.0 and 3.0.0, literally a single API was added:

Comparison of Microsoft.Extensions.Configuration.Abstractions versions using fuget.org

This screenshot was taken from the excellent https://fuget.org using the API diff feature!

That stability means that it may be be possible for you your library to keep targeting the 2.x versions of the libraries. When used in an ASP.NET Core 2.x app, the 2.x.x libraries will be used, just as before. However, when you reference your library in an ASP.NET Core 3.0, the 2.x dependencies of your library will be automatically upgraded to the 3.0.0 versions due to the NuGet package resolution rules.

In general that automatic upgrading is something you want to avoid, as a bump in a major version means breaking changes. You can't guarantee that code compiled against one version of a dependency will run correctly when used against a different major version of the dependency.

However, we've already established that the 3.0.0 version of the libraries are virtually the same, so there's nothing to worry about! To convince you further that this is actually OK, this is the approach used by Serilog's Microsoft.Extensions.Logging integration package. The package keeps targets .NET Standard 2.0 and references the 2.0.0 version of Microsoft.Extensions.Logging, but can happily be used in ASP.NET Core 3.0 apps:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFrameworks>netstandard2.0</TargetFrameworks>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Serilog" Version="2.8.0" />
    <PackageReference Include="Microsoft.Extensions.Logging" Version="2.0.0" />
  </ItemGroup>

</Project>

It's worth pointing out that for .NET Framework targets, you'll need to use binding redirects for the Microsoft.Extensions.* libraries. This is apparently a real pain if you're building a PowerShell module!

Unfortunately, this might not always work for you…

Q: My library uses Microsoft.Extensions.* and needs to use different versions of those libraries when using in .NET Core 2.x vs 3.0

Not all of the library changes are safe to be silently upgraded in this way. For example, consider the Microsoft.Extensions.Options library. In 3.0.0, the Add, Get and Remove methods were removed from OptionsWrapper<>. If you use these methods in your library, then consuming apps running on ASP.NET Core 3.0 will get a MethodNotFoundException at runtime. Not good!

The above example is a bit contrived (it's unlikely you're using OptionsWrapper<> in your libraries), but I've run into this issue a lot when using the IdentityModel library. You have to be very careful to reference the same major version of this library in all your dependencies, otherwise you're likely to get MethodNotFoundExceptions at runtime.

The issue you're likely to see with IdentityModel after upgrading to .NET Core 3.0 is for the CryptoRandom.CreateUniqueId() method. As you can see in the fuget.org comparison below, the default parameters for the method have changed in version 4.0.0. That avoids compile-time breaking changes, but gives a runtime breaking change instead!

The breaking change to IdentityModel moving from 3.10.10 to 4.0.0

So how can you handle this? The best answer I've found is to multi-target .NET Standard 2.0 and .NET Core 3.0, and conditionally include the correct version of your library using MSBuild conditions.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFrameworks>netstandard2.0;netcoreapp3.0</TargetFrameworks>
  </PropertyGroup>
  
  <ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.0'">
    <PackageReference Include="Microsoft.Extensions.Options" Version="3.0.0" />
    <PackageReference Include="IdentityModel" Version="4.0.0" />
  </ItemGroup>
  
  <ItemGroup Condition="'$(TargetFramework)' != 'netcoreapp3.0'">
    <PackageReference Include="Microsoft.Extensions.Options" Version="2.2.0" />
    <PackageReference Include="IdentityModel" Version="3.10.10" />
  </ItemGroup>

</Project>

In the above example, I've shown a library that depends on both Microsoft.Extensions.Options and IdentityModel. Even though technically the latest versions of both of these packages support .NET Standard 2.0, the differences are nuanced, as I've discussed.

When an ASP.NET Core 2.x app depends on the library above, it will use the 2.2.0 version of the *.Options library, and the 3.10.10 version of IdentityModel. When an ASP.NET Core 3.0 app depends on the library above, it will use the 3.0.0 version of the *.Options library, and the 4.0.0 version of IdentityModel.

The main downside to this approach is the increased complexity in tooling. You may need to add #ifdefs around your code to cater to the different target frameworks and libraries. You may also need extra tests. Generally speaking though, this approach is probably the "safest".

There is a scenario I haven't addressed here - if you're running a .NET Core 2.x app (non-ASP.NET Core) and are using the 3.0.0 version Microsoft.Extensions.* libraries (or 4.0.0 version of IdentityModel), and are consuming an app built using the approach shown above. In this case it all falls down. The netstandard2.0 version of the library will be selected, and you could be back in MethodNotFound land. 🙁 Luckily, that seems like a pretty niche and generally unsupported scenario…

Patient saying 'Doc, it hurts when I touch my shoulder'. Doctor saying 'Then don't touch it'

Libraries that depend on ASP.NET Core NuGet packages

This brings us to the final section: libraries that depend on ASP.NET Core-specific libraries. That includes pretty much any library that starts Microsoft.AspNetCore.* (see the migration guide for a complete list). These NuGet packages are no longer being produced and pushed to https://nuget.org, so you can't reference them!

Instead, these are installed as part of the ASP.NET Core 3.0 shared framework. Instead of referencing individual packages, you use a <FrameworkReference> element. This makes all of the APIs in ASP.NET Core 3.0 available. A nice feature of the <FrameworkReference> is that it doesn't need to copy any extra libraries to your app's output folder. MSBuild knows those APIs will be available when the app is executed, so you get a nicely trimmed output.

Not all of the libraries that were in the Microsoft.AspNetCore.App meta package have been moved to the framework. The packages listed in this section of the migration document do still need to be referenced directly, in addition (or instead of) the <FrameworkReference> element. This includes things like EF Core, JSON.NET MVC support, and the Identity UI.

Q: My library only needs to target ASP.NET Core 3.0

This is the simplest scenario, as described in this StackOverflow question - you have a library that uses ASP.NET Core specific features, and you want to upgrade it from 2.x to 3.0.

The solution, as described above, is to remove the obsolete packages, and use a FrameworkReference instead:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
  </ItemGroup>

</Project>

This is actually pretty nice for libraries. All the ASP.NET Core APIs are available to IntelliSense, and you don't have to worry about trying to hunt down the APIs you need in individual packages.

Where things get more complicated again is if you need to support .NET Core 2.x as well.

Q: My library needs to support both ASP.NET Core 2.x and ASP.NET Core 3.0

The only real way to handle this scenario is with the multi-targeting approach we used previously for the Microsoft.Extensions.* (and IdentityModel) libraries. Continue to target .NET Standard 2.0 (to support .NET Core 2.x and .NET Framework 4.6.1+) and also target .NET Core 3.0. Conditionally include either the individual packages for ASP.NET Core 2.x, or the Framework Reference for ASP.NET Core 3.0:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFrameworks>netstandard2.0;netcoreapp3.0</TargetFrameworks>
  </PropertyGroup>
  
  <ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.0'">
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
  </ItemGroup>
  
  <ItemGroup Condition=" '$(TargetFramework)' != 'netcoreapp3.0'">
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Cors" Version="2.1.3" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Formatters.Json" Version="2.1.3" />
    <PackageReference Include="Microsoft.Extensions.Configuration" Version="2.1.1" />
  </ItemGroup>

</Project>

That pretty much covers all the scenarios you should run into. Supporting older versions of the libraries is frustratingly complex, so whether the pay off is worth it, is up to you. But with ASP.NET Core 2.1 being an LTS release for .NET Core (and being supported "forever" on .NET Framework), I suspect many people will be stuck in this situation for a while.

Rather than targeting .NET Standard 2.0, you can also explicitly target .NET Core 2.1 and .NET Framework 4.6.1 as Damian Edwards does in his TagHelperPack. The end result is pretty much the same.

Summary

In this post I tried to break down all the different approaches to upgrading your libraries to support .NET Core 3.0, based on their dependencies. If you don't have any dependencies, or they're isolated from the ASP.NET Core/Microsoft.Extensions.* ecosystem, then you shouldn't have any problems upgrading. If you have Microsoft.Extensions.* dependencies, then you may get away without upgrading your package references, but you might have to conditionally include libraries based on target framework. If you have ASP.NET Core dependencies and need to support for 2.x and 3.0 then you'll almost certainly need to add MSBuild conditionals to your .csproj files.

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