This is a series on the latest 2.* .Net Core bits, Following on from the original .Net Core Series

(At the time of writing, 2.1.4. I use windows, you don't have to!)

Last time we learned how to package our libraries as NuGet packages. But it was only targeting netstandard2.0, the netstandard target framework moniker for version 2.0.

A Quick Reference

All the old profiles and platforms have been mapped to netstandard target versions. This allows you to choose a version of netstandard* to target that will be compatible with a minimum version of a specific platform runtime. Take a look at dotnet standard versions page for both framework and PCL mappings to netstandard versions.

If you implement a particular target version in your package, it can be used by other libraries that are compatible with an equal or higher version level.

The main catch is that whichever version of netstandard you choose, you will be limited to that version's set of APIs in your code, and will not be able to use any API methods, classes or properties that are not in that version.

A very comprehensive list of Target Frameworks can be found in the docs on the target frameworks reference page. You may wish to refer to this going forward, but it is not necessary to read it now.

But as we will see, we are not limited to one version, or even to just netstandard.

Setup

If you have been following the series right through, you should be able to follow what the next script does to set up our starting project:

mkdir MyNewPackage
cd MyNewPackage
mkdir MyLib
cd MyLib
dotnet new classlib
cd ../
dotnet new sln
dotnet sln add ./MyLib/MyLib.csproj
dotnet build

Like before, you may wish to replace Class1.cs with a Calculator class.

using System;

namespace MyLib
{
    public class Calculator
    {
        public int Add(int first, int second)
        {
            return first + second;
        }
    }
}

And one more:

dotnet build

On with the show!

This all boils down to one XML Element inside the *.csproj file.

Starting with the initial MyLib.csproj file:

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

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

</Project>

Say we wanted to target netstandard1.0 instead. It would be as simple as changing "netstandard2.0" to "netstandard1.0". We've covered this, and it works fine.

Now let's say we want to use some netstandard2.0 features if they are available. But we also want to be compatible with netstandard1.0, with limited functionality, or alternative features used. We would do this to our csproj.

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

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

</Project>

Note that we changed <TargetFramework> to <TargetFrameworks>, plural (briefly mentioned earlier on in the series). And by adding new frameworks, we can target multiple platforms. By default, a build will now build all the frameworks, (but there are arguments that allow us to target only one or the other during build).

So let's run a rebuild with this change:

dotnet build
Microsoft (R) Build Engine version 15.5.180.51428 for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.

  Restoring packages for C:\dev\MyNewPackage\MyLib\MyLib.csproj...
  Generating MSBuild file C:\dev\MyNewPackage\MyLib\obj\MyLib.csproj.nuget.g.targets.
  Restore completed in 471.53 ms for C:\dev\MyNewPackage\MyLib\MyLib.csproj.
  MyLib -> C:\dev\MyNewPackage\MyLib\bin\Debug\netstandard1.0\MyLib.dll
  MyLib -> C:\dev\MyNewPackage\MyLib\bin\Debug\netstandard2.0\MyLib.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:03.26

specifically note the two lines near the end:

MyLib -> C:\dev\MyNewPackage\MyLib\bin\Debug\netstandard1.0\MyLib.dll
MyLib -> C:\dev\MyNewPackage\MyLib\bin\Debug\netstandard2.0\MyLib.dll

What does this do to our file tree?

MyNewPackage
|-- MyNewPackage.sln
+-- MyLib
     |-- Class1.cs
     |-- MyLib.csproj
     +-- bin
          +-- Debug
               +-- netstandard1.0
               |    |-- MyLib.deps.json
               |    |-- MyLib.dll
               |    \-- MyLib.pdb
               +-- netstandard2.0
                    |-- MyLib.deps.json
                    |-- MyLib.dll
                    \-- MyLib.pdb

We now have two target outputs, and two dll files, which target different frameworks.

Dependencies

When we add Dependencies to our project, as discussed before, but it is worth noting that by default they apply to all targets. Since we have only had one target until now that hasn't been an explicit thing, but we have two targets in this example.

We don't have to add the dependency to every target, however. We can choose to add dependencies to only a specific target framework using some of the MSBuild features available to us. (This applies to other references too, but I won't cover that here)

Say that we want to reference Json.Net, but only if we are targetting netstandard2.0. This will mean that the library isn't available to our netstandard1.0 version, (and so we would have to conditionally compile out any code that used it), but it would mean that anyone using this library with a netstandard1.0 compatible target would not need the NuGet reference or its library. (A bit contrived, but the main thing to show you is the syntax. In reality, it is more likely you have a library that doesn't support netstandard1.0 and you want graceful degradation on older targets).

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

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

  <ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
    <PackageReference Include="Newtonsoft.Json" Version="11.0.1" />
  </ItemGroup>
</Project>

you can add as many or as few Condition clauses as you need, and this creates conditional PackageReferences, depending on which TargetFramwork is being built at the time.

Conditional Compilation

If we are targeting different frameworks, we likely have to adjust the code as well. We do this using compiler preprocessor symbols.

We get a bunch of symbols created for us, and each target framework will create a few at compile-time automatically for you. We can use these to conditionally compile parts of our code:

#if NETSTANDARD2_0
        //Extra code for netstandard
        var x = 0;
        //...Something using Newtonsoft.Json...
#endif

More information on using compiler preprocessor symbols can be found here.

Packaging

Packaging into a nupkg file works the same as before.

dotnet pack
Microsoft (R) Build Engine version 15.5.180.51428 for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.

  Restore completed in 32.28 ms for C:\dev\MyNewPackage\MyLib\MyLib.csproj.
  MyLib -> C:\dev\MyNewPackage\MyLib\bin\Debug\netstandard1.0\MyLib.dll
  MyLib -> C:\dev\MyNewPackage\MyLib\bin\Debug\netstandard2.0\MyLib.dll
  Successfully created package 'C:\dev\MyNewPackage\MyLib\bin\Debug\MyLib.1.0.0.nupkg'.

And results in a new file:

MyNewPackage
|-- MyNewPackage.sln
+-- MyLib
     |-- Class1.cs
     |-- MyLib.csproj
     +-- bin
          +-- Debug
               |-- MyLib.1.0.0.nupkg
               +-- netstandard1.0
               |    |-- MyLib.deps.json
               |    |-- MyLib.dll
               |    \-- MyLib.pdb
               +-- netstandard2.0
                    |-- MyLib.deps.json
                    |-- MyLib.dll
                    \-- MyLib.pdb

Instead of getting a package per target, we get one package that contains both targets.

MyLib.1.0.0.nupkg  
|-- MyLib.nuspec
+-- lib
     +-- netstandard1.0
     |    \-- MyLib.dll
     +-- netstandard1.6
          \-- MyLib.dll

A sidenote on running

You can also use multi-targeting on an application. When you want to run an application with multiple targets, you can use the dotnet run command with the -f <target> flag option.

dotnet run -f netcoreapp1.0

Of course, netcoreapp1.0 is the application target for the .Net Core runtime, compared to using net45 perhaps to target .Net 4.5 runtime instead.

net40

The target netstandard1.0 is compatible with .Net 4.5 and newer, UWP apps, Windows 8 & Windows Phone 8 or newer, Mono 4.6 and certain Xamarin versions, and of course new .Net Core applications. Also, netstandard2.0 is compatible with .Net 4.6.1 and newer, newer UWP versions, Mono 5.4 and newer Xamarin versions, and .Net Core 2.0+. But we all know that there are a ton of applications out there targeting .Net 4.0, and even .Net 3.5 that we still maintain and support that we might want our library packages to target. (Yes, still. It is out of support but that doesn't stop some people... -so is 4.5 now, BTW)

But we can easily target net40 and net35 as well (though not net35 on my windows 10 machine... without some extra SDKs installed).

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

  <PropertyGroup>
    <TargetFrameworks>netstandard1.0;netstandard2.0;net45;net40</TargetFrameworks>
  </PropertyGroup>

  <ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
    <PackageReference Include="Newtonsoft.Json" Version="11.0.1" />
  </ItemGroup>
</Project>

This simple library doesn't need anything outside the standard library, but if it did, we would add in target-specific packages and libraries to the net40 and net45 targets as well.

dotnet build
Microsoft (R) Build Engine version 15.5.180.51428 for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.

  Restoring packages for C:\dev\MyNewPackage\MyLib\MyLib.csproj...
  Restore completed in 174.61 ms for C:\dev\MyNewPackage\MyLib\MyLib.csproj.
  MyLib -> C:\dev\MyNewPackage\MyLib\bin\Debug\netstandard1.0\MyLib.dll
  MyLib -> C:\dev\MyNewPackage\MyLib\bin\Debug\net40\MyLib.dll
  MyLib -> C:\dev\MyNewPackage\MyLib\bin\Debug\net45\MyLib.dll
  MyLib -> C:\dev\MyNewPackage\MyLib\bin\Debug\netstandard2.0\MyLib.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:03.37

We can pack this.

dotnet pack
Microsoft (R) Build Engine version 15.5.180.51428 for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.

  Restore completed in 29.08 ms for C:\dev\MyNewPackage\MyLib\MyLib.csproj.
  MyLib -> C:\dev\MyNewPackage\MyLib\bin\Debug\netstandard1.0\MyLib.dll
  MyLib -> C:\dev\MyNewPackage\MyLib\bin\Debug\net40\MyLib.dll
  MyLib -> C:\dev\MyNewPackage\MyLib\bin\Debug\net45\MyLib.dll
  MyLib -> C:\dev\MyNewPackage\MyLib\bin\Debug\netstandard2.0\MyLib.dll
  Successfully created package 'C:\dev\MyNewPackage\MyLib\bin\Debug\MyLib.1.0.0.nupkg'.

Then we will get:

MyNewPackage
|-- MyNewPackage.sln
+-- MyLib
     |-- Class1.cs
     |-- MyLib.csproj
     +-- bin
          +-- Debug
               |-- MyLib.1.0.0.nupkg
               +-- net40
               |    |-- MyLib.dll
               |    \-- MyLib.pdb
               +-- net45
               |    |-- MyLib.dll
               |    \-- MyLib.pdb
               +-- netstandard1.0
               |    |-- MyLib.deps.json
               |    |-- MyLib.dll
               |    \-- MyLib.pdb
               +-- netstandard2.0
                    |-- MyLib.deps.json
                    |-- MyLib.dll
                    \-- MyLib.pdb

And:

MyLib.1.0.0.nupkg  
|-- MyLib.nuspec
+-- lib
     +-- net40
     |    \-- MyLib.dll
     +-- net45
     |    \-- MyLib.dll
     +-- netstandard1.0
     |    \-- MyLib.dll
     +-- netstandard1.6
          \-- MyLib.dll

What's Left?

That should give you a reasonable (albeit simple) overview of multi-targeting libraries. The last focus is on applications, and how we publish and distribute these using the dotnet CLI tools. Up Next!