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.
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 .
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).
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.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>
netstandard2.0
and net5.0
.9.0
so that I can use all the latest bells and whistlesNullable
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With