Multi-targeting a library in the csproj is straightforward. All you need to do is list all target frameworks in the TargetFrameworks element:
csproj (MSBuild project file)
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net5.0;netstandard2.1;netstandard2.0;net461</TargetFrameworks>
</PropertyGroup>
</Project>
Then, you may need different implementations depending on the target framework. When MSBuild compiles your project, it automatically defines a preprocessor symbol you can use in #if. For instance, you can use #if NET461 ... #endif to compile code specific to the net461 target.
C#
#if NET461
// Code specific to net461
#elif NETSTANDARD2_0 || NETSTANDARD2_1
// Code specific to netstandard2.1 or netstandard2.0
#else
// Code specific to other frameworks
#endif
The pattern I often see in codebases is:
C#
#if NET5_0
// Optimized code that uses .NET 5.0 API
#else
// Legacy implementation for previous frameworks
#endif
This code works fine until you add a new target framework. Newer frameworks may then unintentionally fall through to the legacy implementation. Tracking down every #if that needs updating is error-prone. An easy way to prevent this is to add a #else branch that reports an error using #error.
C#
#if NET5_0
// Optimized code that uses .NET 5.0 API
#elif NETSTANDARD2_0 || NETSTANDARD2_1
// Legacy implementation for previous frameworks
#else
// Prevent compilation for unknown target frameworks
#error Target Framework not supported
#endif
Adding a new target framework now produces a compilation error. Note that the error list shows which target triggered the error (netcoreapp3.1 in this case).

You can now check all locations that need to be updated when adding a new TFM.
Future versions of .NET may provide OR_GREATER preprocessor symbols for TFMs to enable forward-compatible #if conditions, such as NET5_0_OR_GREATER. See the accepted proposal "OR_GREATER preprocessor symbols for TFMs proposal".
#Additional resources
Do you have a question or a suggestion about this post? Contact me!