When building a nuget package, you often face the following "dilemma":
You want to depend on the lowest possible framework version (to ensure maximum reach), but you also want to take advantage of the latest features in dotnet.

Can we have the cookie and eat it too?

Multi-targeting in dotnet

By using multi-targeting we can support different framework versions at the same time.

Let's have a look at the setup of my JOS.Enumeration package (I'm using [Directory.Build.props] to enable easier maintanance of my csproj files.).

<Project>
    <PropertyGroup>
        <TargetFrameworks>net7.0;net8.0</TargetFrameworks>
        <Nullable>enable</Nullable>
        <ImplicitUsings>disable</ImplicitUsings>
        <LangVersion>preview</LangVersion>
        <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
        <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
    </PropertyGroup>
</Project>

As you can see, my library supports both net7.0 and net8.0.

Framework specific code.

In dotnet 8, a new collection type has been added, FrozenSet.
It has the following characteristics:

Provides an immutable, read-only set optimized for fast lookup and enumeration.

Sounds like something I want to use in my library!
The following code is used for the net7.0 version:

private static readonly IReadOnlySet<Hamburger> AllItems;

static Hamburger()
{
    AllItems = new HashSet<Hamburger>()
    {
        ......
    };
}

It uses a plain "old" HashSet. However, if the consumer of my library are using net8.0, the following code will be used instead:

private static readonly IReadOnlySet<Hamburger> AllItems;

static Hamburger()
{
    AllItems = new HashSet<Hamburger>()
    {
        ....
    }.ToFrozenSet();
}

This is achieved by using conditional compilation symbols like this:

private static readonly IReadOnlySet<Hamburger> AllItems;

static Hamburger()
{
    #if NET8_0_OR_GREATER
    AllItems = new HashSet<Hamburger>()
    {
        ...
    }.ToFrozenSet();
    #else
    AllItems = new HashSet<Hamburger>()
    {
        ...
    };
    #endif
}

See it in action here.

External dependencies

It's common that you want to use different versions of your external dependecies depending on the framework version that's used.
When using net7.0, you might want to use version 7 of EF Core for example.

I use central package management to unify the handling of external packages in my projects.

I structure my Directory.Packages.props like this:(you can see it in action here)

  • Common dependencies between framework versions
  • Framework specific dependencies.
<Project>
    <PropertyGroup>
        <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
    </PropertyGroup>
    <ItemGroup>
        <PackageVersion Include="BenchmarkDotNet" Version="0.13.7"/>
        <PackageVersion Include="Dapper" Version="2.0.151"/>
        <PackageVersion Include="EFCore.NamingConventions" Version="7.0.2"/>
        <PackageVersion Include="JOS.Enumeration" Version="$(NBGV_NuGetPackageVersion)"/>
        <PackageVersion Include="JOS.Enumeration.SourceGenerator" Version="$(NBGV_NuGetPackageVersion)"/>
        <PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1"/>
        <PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4"/>
        <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0"/>
        <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.7.1"/>
        <PackageVersion Include="Respawn" Version="6.1.0"/>
        <PackageVersion Include="Respawn.Postgres" Version="1.0.15"/>
        <PackageVersion Include="Shouldly" Version="4.2.1"/>
        <PackageVersion Include="xunit" Version="2.5.0"/>
        <PackageVersion Include="xunit.runner.visualstudio" Version="2.5.0"/>
    </ItemGroup>
    <ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
        <PackageVersion Include="Microsoft.EntityFrameworkCore" Version="7.0.10"/>
        <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.7"/>
        <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0"/>
        <PackageVersion Include="Microsoft.Extensions.Hosting" Version="7.0.1"/>
        <PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="7.0.0"/>
        <PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.4"/>
    </ItemGroup>
    <ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
        <PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.0-preview.7.23375.4"/>
        <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0-preview.7.23375.4"/>
        <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0-preview.7.23375.6"/>
        <PackageVersion Include="Microsoft.Extensions.Hosting" Version="8.0.0-preview.7.23375.6"/>
        <PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0-preview.7.23375.6"/>
        <PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0-preview.7"/>
    </ItemGroup>
    <ItemGroup>
        <GlobalPackageReference Include="Nerdbank.GitVersioning" Version="3.6.133">
            <PrivateAssets>all</PrivateAssets>
        </GlobalPackageReference>
    </ItemGroup>
</Project>

When building the project for net7.0, net7.0 specific dependencies are used. The same goes for net8.0.

Building and publishing the packages

I'm using GitHub actions for releasing my packages.
The only thing that differs from a single target nuget package is that you'll need to ensure that you specify all the frameworks you support in the setup-dotnet step.

    steps:
    ...
    - name: Setup .NET
      uses: actions/setup-dotnet@v3
      with:
        dotnet-version: |
          7.0.400
          8.0.x
        dotnet-quality: 'preview'
     ...

You don't need to change anything else, just run dotnet build, dotnet pack and dotnet publish as usual.
A full example of my pipeline configuration can be found here.