Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Adding .NET 5.0 target to my NuGet packages

I have a few dozen NuGet packages published. Most target .NET Standard. Visual Studio makes it very easy to update these packages each time I compile.

Now, I'd like to upgrade these packages to take full advantage of .NET 5.0. But I want the existing ones to still be available for those unable to upgrade to .NET 5.0. Also, I don't want to create completely new packages, such that I have two versions of each one.

If I'm not mistaken, a NuGet package can target multiple framework versions, and I think Visual Studio will automatically load the correct one when installed into a project. However, creating multiple package targets isn't baked into Visual Studio.

I don't know if there are any NuGet package experts here. I would like information about if this is possible, what's the easiest way to accomplish it, how to you handle versioning, etc. Any good book or article references?

Additional Notes

I know I can target multiple frameworks at the project level (using the TargetFrameworks element in the project file). But then I have to fill my code if #if, else, endif blocks to take advantage of each target framework. I don't think I want this. My library method signatures will change to take advantage of things like nullable strings. I want to be able to do this freely without create multiple versions of everything.

like image 548
Jonathan Wood Avatar asked Feb 05 '21 14:02

Jonathan Wood


People also ask

How do I choose the target version of .NET standard library?

If you are not sure which version of . NET Standard you should target, go with 2.0 as it offers a balance of reach and APIs available for you to use. If your goal is to make your library usable for many frameworks as possible while gettings all the APIs you can from .


1 Answers

The way to go is definitely to multi-target your nugets with <TargetFrameworks>netstandard2.0;net5.0</TargetFrameworks> in your csproj. Then to also enable nullables and set LangVersion to v9.0.

Once this is done, depending on your existing code, you'll probably have to adapt the code depending on the target, as everything .NET 5 won't be supported in bare .Net Standard 2...

However, although I doubt you can completely get rid of #if guards, there are a few solutions out there.

First we need to distinguish between 3 kinds of novelties that appeared after netstandard2.0 (that is after .NET 4.6.1 and .NET Core 2.1 eras).

  • Compiler-dependent only new features: these features only require a recent compiler, but once compiled can be consumed by old versions of .NET because they maintain complete IL compatibility and do not require new types in the runtime assemblies. Throw-expressions, static (or not) local functions for example.
  • Runtime-dependent features: these ones (such as Span<T>) require an up-to-date CLR. These are the ones you want to avoid (at least in the public interface) if the nuget packages must support .NET Standard 2 targets.
  • In-between features ;) These features are mainly supported by the compiler but come with not-so-important support in the runtime. Nullables and records fall into this category: they rely on a few attributes (and some new IL metadata in the record case), but given these attributes are provided at runtime, they can be consumed in netstandard2.0 targets.

Now, let's see what we can do to avoid too much #if. The first place to go is the csproj:

<PropertyGroup>
  <TargetFrameworks>netstandard2.0;net5.0</TargetFrameworks>
  <!-- enable the feature but without warning for legacy frameworks to avoid false positives. -->
  <Nullable>annotations</Nullable>
  <!-- enable the feature including warnings only on top of the most recent framework you target. -->
  <!-- it will serve as a reference for warnings for all others. -->
  <Nullable Condition="'$(TargetFramework)' == 'net5.0'">enable</Nullable>
  <LangVersion>9.0</LangVersion>
</PropertyGroup>
  • I'm targeting netstandard2.0 and net5.0.
  • I set the language version to 9.0 so that I can use all the latest bells and whistles
  • The declaration of Nullable is a bit convoluted, but basically, it prevents warnings from popping up for the netstandard2.0 target whereas having the full support for the .net5.0 target.

Then let's add these two magical nuget packages:

<ItemGroup>
  <PackageReference Include="IsExternalInit" Version="1.0.0">
    <PrivateAssets>all</PrivateAssets>
    <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
  </PackageReference>
  <PackageReference Include="Nullable" Version="1.3.0">
    <PrivateAssets>all</PrivateAssets>
    <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
  </PackageReference>
</ItemGroup>

These 2 packages are provided by the same person (See here and here). They are dev-only dependencies and as such won't pollute your consumers down the dependency chain. What they do is simply declare the attributes required for supporting nullables and records (as internal classes so as not to collide with legitimate ones).

With this setup, you can now use records and nullables in your code base and it will compile successfully to both targets.

If you consume the assembly from a net5.0 client, everything will work as expected. Then if you consume it from, say, a .NET 4.8 assembly things will be ok too: sure, you won't have nullable warnings or intellisense and the records will be considered regular classes, but I'd say it nicely degrades.

For the last kind of features, the really runtime-dependent ones, you'll have no choice but to #if if you want to use them. Say you'd like to replace some array based code with Span<T>... You'll need to provide two private implementations and #if select the correct one depending on the target (See https://learn.microsoft.com/en-us/dotnet/standard/frameworks#how-to-specify-a-target-framework for a list of supported preprocessor constants).

I didn't exhaustively covered everything that changed between .NET Standard 2 and .NET 5 (that'd probably be a book), but that should get you started. I'm sure that feature by feature, clever people found solutions to shim them to older versions of .NET if possible.

I also pushed my test playground to this github repository if you want to have a look.

And as a final note, I'd say the most important thing is to have thorough tests sa as to make sure modernizing the code base does not break anything for older clients.

like image 83
odalet Avatar answered Oct 19 '22 21:10

odalet