title | description | keywords | author | manager | ms.date | ms.topic | ms.prod | ms.technology | ms.devlang | ms.assetid |
---|---|---|---|---|---|---|---|---|---|---|
Developing Libraries with Cross Platform Tools |
Developing Libraries with Cross Platform Tools |
.NET, .NET Core |
cartermp |
wpickett |
06/20/2016 |
article |
.net-core |
.net-core-technologies |
dotnet |
9f6e8679-bd7e-4317-b3f9-7255a260d9cf |
Some details are subject to change as the toolchain evolves.
This article covers how you can write libraries for .NET using cross-platform CLI tools. They provide an efficient and low-level experience that works across any supported OS. You can still build libraries with Visual Studio, and if that is your preferred experience then you should refer to the Visual Studio guide.
You must have .NET Core installed on your machine. You will need the .NET Core SDK and CLI.
The sections of this document dealing with the .NET Framework versions or Portable Class Libraries (PCL) need the .NET Framework installed. They are only supported on Windows. To do this, install the .NET Framework.
Additionally, if you wish to support older targets, you will need to install targeting/developer packs for older framework versions from the target platforms page. Refer to this table:
.NET Framework Version | What to download |
---|---|
4.6 | .NET Framework 4.6 Targeting Pack |
4.5.2 | .NET Framework 4.5.2 Developer Pack |
4.5.1 | .NET Framework 4.5.1 Developer Pack |
4.5 | Windows Software Development Kit for Windows 8 |
4.0 | Windows SDK for Windows 7 and .NET Framework 4 |
2.0, 3.0, and 3.5 | .NET Framework 3.5 SP1 Runtime (or Windows 8+ version) |
If you're not quite familiar with the .NET Standard, please refer to the .NET Standard Library to learn more.
In that article, there is a table which maps .NET Standard versions to various implementations:
Platform Name | Alias | |||||||
---|---|---|---|---|---|---|---|---|
.NET Standard | netstandard | 1.0 | 1.1 | 1.2 | 1.3 | 1.4 | 1.5 | 1.6 |
.NET Core | netcoreapp | → | → | → | → | → | → | 1.0 |
.NET Framework | net | → | 4.5 | 4.5.1 | 4.6 | 4.6.1 | 4.6.2 | 4.6.3 |
Mono/Xamarin Platforms | → | → | → | → | → | → | * | |
Universal Windows Platform | uap | → | → | → | → | 10.0 | ||
Windows | win | → | 8.0 | 8.1 | ||||
Windows Phone | wpa | → | → | 8.1 | ||||
Windows Phone Silverlight | wp | 8.0 |
Here's what this table means for the purposes of creating a library:
The version of the .NET Platform Standard you pick will be a tradeoff between access to the newest APIs and ability to target more .NET platforms and Framework versions. You can do that by picking a version of netstandardXX
(Where XX
is a version number) and adding it to your project.json
file.
Additionally, the corresponding NuGet package to depend on is NETStandard.Library
version 1.6.0
. Although there's nothing preventing you from depending on Microsoft.NETCore.App
like with console apps, it's generally not recommended. If you need APIs from a package not specified in NETStandard.Library
, you can always specify that package in addition to NETStandard.Library
in the dependencies
section of your project.json
file.
You have three primary options when targeting the .NET Standard, depending on your needs.
-
You can use the latest version of the .NET Standard -
netstandard1.6
- which is for when you want access to the most APIs and don't mind if you have less reach across implementations. -
You can use a lower version of the .NET Standard to target earlier .NET implementations. The cost here is not having access to some of the latest APIs.
For example, if you wanted to have guaranteed compatibility with .NET Framework 4.6 and higher, you would pick
netstandard1.3
:{ "dependencies":{ "NETStandard.Library":"1.6.0" }, "frameworks":{ "netstandard1.3":{} } }
The .NET Standard versions in a backward-compatible way. That means that
netstandard1.0
libraries run onnetstandard1.1
platforms and higher. However, there is no forwards-compatibility - lower .NET Standard platforms cannot reference higher ones. This means thatnetstandard1.0
libraries cannot reference libraries targetingnetstandard1.1
or higher. You should select the Standard version that has the right mix of APIs and platform support for your needs. -
If you want to target the .NET Framework versions 4.0 or below, or you wish to use an API available in the .NET Framework but not in the .NET Standard (for example,
System.Drawing
), read the following sections and learn how to multitarget.
NOTE: These instructions assume you have the .NET Framework installed on your machine. Refer to the Prerequisites to get dependencies installed.
Keep in mind that some of the .NET Framework versions used here are no longer in support. Refer to the .NET Framework Support Lifecycle Policy FAQ about unsupported versions.
If you want to reach the maximum developers and projects, use the .NET Framework 4 as your baseline target. To target the .NET Framework, you will need to begin by using the correct Target Framework Moniker (TFM) that corresponds to the .NET Framework version you wish to support.
.NET Framework 2.0 --> net20
.NET Framework 3.0 --> net30
.NET Framework 3.5 --> net35
.NET Framework 4.0 --> net40
.NET Framework 4.5 --> net45
.NET Framework 4.5.1 --> net451
.NET Framework 4.5.2 --> net452
.NET Framework 4.6 --> net46
.NET Framework 4.6.1 --> net461
.NET Framework 4.6.2 --> net462
.NET Framework 4.6.3 --> net463
For example, here's how you would write a library which targets the .NET Framework 4:
{
"frameworks":{
"net40":{}
}
}
And that's it! Although this compiled only for the .NET Framework 4, you can use the library on newer versions of the .NET Framework.
NOTE: These instructions assume you have the .NET Framework installed on your machine. Refer to the Prerequisites to get dependencies installed.
Targeting a PCL profile is a bit trickier than targeting .NET Standard or the .NET Framework. For starters, reference this list of PCL profiles to find the NuGet target which corresponds to the PCL profile you are targeting.
Then, you need to do the following:
- Create a new entry under
frameworks
in yourproject.json
, named.NETPortable,Version=v{version},Profile=Profile{profile}
, where{version}
and{profile}
correspond to a PCL version number and Profile number, respectively. - In this new entry, list every single assembly used for that target under a
frameworkAssemblies
entry. This includesmscorlib
,System
, andSystem.Core
. - If you are multitargeting (see the next section), you must explicitly list dependencies for each target under their target entries. You won't be able to use a global
dependencies
entry anymore.
The following is an example targeting PCL Profile 328. Profile 328 supports: .NET Standard 1.4, .NET Framework 4, Windows 8, Windows Phone 8.1, Windows Phone Silverlight 8.1, and Silverlight 5.
{
"frameworks":{
".NETPortable,Version=v4.0,Profile=Profile328":{
"frameworkAssemblies":{
"mscorlib":"",
"System":"",
"System.Core":""
}
}
}
}
You can now build.
$ dotnet restore
$ dotnet build
Notice the following entry in the /bin/Debug
folder:
$ ls bin/Debug
portable-net40+sl50+netcore45+wpa81+wp8/
This folder contains the .dll
files necessary to run your library.
NOTE: These following instructions assume you have the .NET Framework installed on your machine. Refer to the Prerequisites section to learn which dependencies you need to install and where to download them from.
You may need to target older versions of the .NET Framework when your project supports both the .NET Framework and .NET Core. In this scenario, if you want to use newer APIs and language constructs for the newer targets, use #if
directives in your code. You also might need to add different packages and dependencies in your project.json file
for each platform you're targeting to include the different APIs needed for each case.
For example, let's say you have a library that performs networking operations over HTTP. For .NET Standard and the .NET Framework versions 4.5 or higher, you can use the HttpClient
class from the System.Net.Http
namespace. However, earlier versions of the .NET Framework don't have the HttpClient
class, so you could use the WebClient
class from the System.Net
namespace for those instead.
So, the project.json
file could look like this:
{
"frameworks":{
"net40":{
"frameworkAssemblies": {
"System.Net":"",
"System.Text.RegularExpressions":""
}
},
"net452":{
"frameworkAssemblies":{
"System.Net":"",
"System.Net.Http":"",
"System.Text.RegularExpressions":"",
"System.Threading.Tasks":""
}
},
"netstandard1.6":{
"dependencies": {
"NETStandard.Library":"1.6.0",
}
}
}
}
Note that the .NET Framework assemblies need to be referenced explicitly in the net40
and net452
target, and NuGet references are also explicitly listed in the netstandard1.6
target. This is required in multitargeting scenarios.
Next, the using
statements in your source file can be adjusted like this:
#if NET40
// This only compiles for the .NET Framework 4 targets
using System.Net;
#else
// This compiles for all other targets
using System.Net.Http;
using System.Threading.Tasks;
#endif
The build system is aware of the following preprocessor symbols used in #if
directives:
.NET Framework 2.0 --> NET20
.NET Framework 3.5 --> NET35
.NET Framework 4.0 --> NET40
.NET Framework 4.5 --> NET45
.NET Framework 4.5.1 --> NET451
.NET Framework 4.5.2 --> NET452
.NET Framework 4.6 --> NET46
.NET Framework 4.6.1 --> NET461
.NET Framework 4.6.2 --> NET462
.NET Standard 1.0 --> NETSTANDARD1_0
.NET Standard 1.1 --> NETSTANDARD1_1
.NET Standard 1.2 --> NETSTANDARD1_2
.NET Standard 1.3 --> NETSTANDARD1_3
.NET Standard 1.4 --> NETSTANDARD1_4
.NET Standard 1.5 --> NETSTANDARD1_5
.NET Standard 1.6 --> NETSTANDARD1_6
And in the middle of the source, you can use #if
directives to use those libraries conditionally. For example:
public class Library
{
#if NET40
private readonly WebClient _client = new WebClient();
private readonly object _locker = new object();
#else
private readonly HttpClient _client = new HttpClient();
#endif
#if NET40
// .NET Framework 4.0 does not have async/await
public string GetDotNetCount()
{
string url = "http://www.dotnetfoundation.org/";
var uri = new Uri(url);
string result = "";
// Lock here to provide thread-safety.
lock(_locker)
{
result = _client.DownloadString(uri);
}
int dotNetCount = Regex.Matches(result, ".NET").Count;
return $"Dotnet Foundation mentions .NET {dotNetCount} times!";
}
#else
// .NET 4.5+ can use async/await!
public async Task<string> GetDotNetCountAsync()
{
string url = "http://www.dotnetfoundation.org/";
// HttpClient is thread-safe, so no need to explicitly lock here
var result = await _client.GetStringAsync(url);
int dotNetCount = Regex.Matches(result, ".NET").Count;
return $"dotnetfoundation.orgmentions .NET {dotNetCount} times in its HTML!";
}
#endif
}
Now you can build.
$ dotnet restore
$ dotnet build
Your /bin/Debug
folder will look like this:
$ ls bin/Debug
net40/
net45/
netstandard1.6/
If you want to cross-compile with a PCL target, you must add a build definition in your project.json
file under buildOptions
in your PCL target. You can then use #if
directives in the source which use the build definition as a preprocessor symbol.
For example, if you want to target PCL profile 328 (The .NET Framework 4, Windows 8, Windows Phone Silverlight 8, Windows Phone 8.1, Silverlight 5), you could to refer to it to as "PORTABLE328" when cross-compiling. Simply add it to the project.json
file as a buildOptions
attribute:
{
"frameworks":{
"netstandard1.6":{
"dependencies":{
"NETStandard.Library":"1.6.0",
}
},
".NETPortable,Version=v4.0,Profile=Profile328":{
"buildOptions": {
"define": [ "PORTABLE328" ]
},
"frameworkAssemblies":{
"mscorlib":"",
"System":"",
"System.Core":"",
"System.Net"
}
}
}
}
Now you can conditionally compile against that target:
#if !PORTABLE328
using System.Net.Http;
using System.Threading.Tasks;
// Potentially other namespaces which aren't compatible with Profile 328
#endif
Because PORTABLE328
is now recognized by the compiler, the PCL Profile 328 library generated by a compiler will not include System.Net.Http
or System.Threading.Tasks
.
Now you can build.
$ dotnet restore
$ dotnet build
Your /bin/Debug
folder will look like this:
$ ls bin/Debug
portable-net40+sl50+netcore45+wpa81+wp8/
netstandard1.6/
You may wish to write a library which depends on a native .dll
file. If you're writing such a library, you have have two options:
- Reference the native
.dll
directly in yourproject.json
. - Package that
.dll
into its own NuGet package and depend on that package.
For the first option, you'll need to include the following in your project.json
file:
- Setting
allowUnsafe
totrue
in abuildOptions
section. - Specifying the path to the native
.dll
(s) with a Runtime Identifier (RID) underfiles
in thepackOptions
section.
If you're distributing your library as a package, it's recommended that you place the .dll
file at the root level of your project. Here's an example project.json
for a native .dll
file that runs on Windows x64:
{
"buildOptions":{
"allowUnsafe":true
},
"packOptions":{
"files":{
"runtimes/win7-x64/native/":"native-lib.dll"
}
}
}
For the second option, you'll need to build a NuGet package out of your .dll
file(s), host on a NuGet or MyGet feed, and depend on it directly. You'll still need to set allowUnsafe
to true
in the buildOptions
section of your project.json
. Here's an example (assuming MyNativeLib
is a Nuget package at version 1.2.0
):
{
"buildOptions":{
"allowUnsafe":true
},
"dependencies":{
"MyNativeLib":"1.2.0"
}
}
To see an example of packaging up cross-platform native binaries, check out the ASP.NET Libuv Package and the corresponding reference in KestrelHttpServer.
It's important to be able to test across platforms. It's easiest to use xUnit, which is also the testing tool used by .NET Core projects. Setting up your solution with test projects will depend on the structure of your solution. The following example assumes that all source projects are under a top-level /src
folder and all test projects are under a top-level /test
folder.
-
Ensure you have a
global.json
file at the solution level which understands where the test projects are:{ "projects":[ "src", "test"] }
Your solution folder structure should then look like this:
/SolutionWithSrcAndTest |__global.json |__/src |__/test
-
Create a new test project by creating a
project.json
file under your/test
folder. You can also run thedotnet new
command and modify theproject.json
file afterwards. It should have the following:netcoreapp1.0
listed as the only entry underframeworks
.- A reference to
Microsoft.NETCore.App
version1.0.0
. - A reference to xUnit version
2.2.0-beta2-build3300
. - A reference to
dotnet-test-xunit
version1.0.0-preview2-build1029
- A project reference to the library being tested.
- The entry
"testRunner":"xunit"
.
Here's an example (
LibraryUnderTest
version1.0.0
is the library being tested):{ "testRunner":"xunit", "dependencies":{ "LibraryUnderTest":{ "version":"1.0.0", "target":"project" }, "Microsoft.NETCore.App":{ "version":"1.0.0", "type":"platform" }, "xunit":"2.2.0-beta2-build3300", "dotnet-test-xunit":".0.0-preview2-build1029", }, "frameworks":{ "netcoreapp1.0":{} } }
-
Restore packages by running
dotnet restore
. You should do this at the solution level if you haven't restored packages yet. -
Navigate to your test project and run tests with
dotnet test
:$ cd path-to-your-test-project $ dotnet test
And that's it! You can now test your library across all platforms using command line tools. To continue testing now that you have everything set up, testing your library is very simple:
- Make changes to your library.
- Run tests from the command line, in your test directory, with
dotnet test
command.
Your code will be automatically rebuilt when you invoke dotnet test
command.
Just remember to run dotnet restore
from the command line any time you add a new dependency and you'll be good to go!
A common need for larger libraries is to place functionality in different projects.
Imagine you wished to build a library which could be consumed in idiomatic C# and F#. That would mean that consumers of your library consume them in ways which are natural to C# or F#. For example, in C# you might consume the library like this:
var convertResult = await AwesomeLibrary.ConvertAsync(data);
var result = AwesomeLibrary.Process(convertResult);
In F#, it might look like this:
let result =
data
|> AwesomeLibrary.convertAsync
|> Async.RunSynchronously
|> AwesomeLibrary.process
Consumtion scenarios like this mean that the APIs being accessed have to have a different structure for C# and F#. A common approach to accomplishing this is to factor all of the logic of a library into a core project, with C# and F# projects defining the API layers that call into that core project. The rest of the section will use the following names:
- AwesomeLibrary.Core - A core project which contains all logic for the library
- AwesomeLibrary.CSharp - A project with public APIs intended for consumption in C#
- AwesomeLibrary.FSharp - A project with public APIs intended for consumption in F#
To reference a project, you need to do two things:
- Understand the name and version number of the project you wish to reference.
- List that project as a dependency using the name and version number from (1).
In the above case, you may wish to set up the project.json
for AwesomeLibrary.Core as follows:
{
"name":"AwesomeLibrary.Core",
"version":"1.0.0"
}
You can use these entries in the project.json
to control the name and version of the project. If you don't specify these, the default configuration is to use the name of the containing folder as the name and 1.0.0 as the version number.
The project.json
files for both AwesomeLibrary.CSharp and AwesomeLibrary.FSharp now need to reference AwesomeLibrary.Core as a project
target. If you aren't multitargeting, you can use the global dependencies
entry:
{
"dependencies":{
"AwesomeLibrary.Core":{
"version":"1.0.0",
"target":"project"
}
}
}
Note: Failure to list the reference as a
project
target may result in NuGet resolving the dependency with an existing NuGet package which happens to have the same name. Always specify"target":"project"
when referencing a project in the same solution.
If you are multitargeting, you may not be able to use a global dependencies
entry and may have to reference AwesomeLibrary.Core in a target-level dependencies
entry. For example, if you were targeting netstandard1.6
, you could do so like this:
{
"frameworks":{
"netstandard1.6":{
"dependencies":{
"AwesomeLibrary.Core":{
"version":"1.0.0",
"target":"project"
}
}
}
}
}
Another important aspect of multi-project solutions is establishing a good overall project structure. To structure a multi-project library, you must use top-level /src
and /test
folders:
/AwesomeLibrary
|__global.json
|__/src
|__/AwesomeLibrary.Core
|__Source Files
|__project.json
|__/AwesomeLibrary.CSharp
|__Source Files
|__project.json
|__/AwesomeLibrary.FSharp
|__Source Files
|__project.json
/test
|__/AwesomeLibrary.Core.Tests
|__Test Files
|__project.json
|__/AwesomeLibrary.CSharp.Tests
|__Test Files
|__project.json
|__/AwesomeLibrary.FSharp.Tests
|__Test Files
|__project.json
The global.json
file for this solution would look like this:
{
"projects":["src", "test"]
}
This approach follows the same pattern established by project templates in the dotnet new
command establish, where all projects are placed under a /src
directory and all tests are placed under a /test
directory.
Here's how you could restore packages, build, and test your entire project:
$ dotnet restore
$ cd src/AwesomeLibrary.FSharp
$ dotnet build
$ cd ../AwesomeLibrary.CSharp
$ dotnet build
$ cd ../../test/AwesomeLibrary.Core.Tests
$ dotnet test
$ cd ../AwesomeLibrary.CSharp.Tests
$ dotnet test
$ cd ../AwesomeLibrary.FSharp.Tests
$ dotnet test
And that's it!