Using Visual Studio Team Services Libraries from .NET Core

TL;DR;

If you want to use the current versions of the .NET wrappers for the VSTS REST APIs in a .NET Core application, you'll need to add the following to your *.csproj file to allow NuGet to fallback to using PCLs based on profiles:

<PropertyGroup>  
    <PackageTargetFallback>portable-net451+win8</PackageTargetFallback>
</PropertyGroup>  

Background

When you are building a product it is impossible to be all things to all people and so you focus on the core scenarios and ideally provide extensibility points to your customers to fill in the gaps that are unique to them. Visual Studio Team Services is no different in this regard. Ever since Team Foundation Server was first released it has had APIs that developers could use to automate various scenarios - those APIs came in the form of very rich .NET class libraries that pretty much allowed you to do anything that Team Explorer did inside Visual Studio and then some.

When Visual Studio Team Services became a thing the VSTS team decided that they needed to provide a set of REST APIs. The benefit with REST APIs is that they are more accessible to all platforms - still, lots of people wanted to integrate using .NET and so a new set of class libraries were developed which wrap the REST APIs to cover the expanding set of capabilities that they provide.

The .NET libraries that wrap the REST APIs are distributed as NuGet packages. Here are some of they key ones that you need to know about:

  • Microsoft.VisualStudio.Services.Client (NuGet)
  • Microsoft.VisualStudio.Services.InteractiveClient (NuGet)
  • Microsoft.TeamFoundationServer.Client (NuGet)
  • Microsoft.TeamFoundationServer.ExtendedClient (NuGet)

Generally speaking the Microsoft.VisualStudio.Services.* packages contain components that are specific to VSTS, whereas the Microsoft.TeamFoundationServer.* packages contain components which work across both VSTS and TFS. The *.InteractiveClient and *.ExtendedClient packages contain components with additional dependencies that aren't useful in all applications (for example, you aren't going to pop up an interactive login prompt for a web-based application so you would reference just Microsoft.VisualStudio.Services.Client and Microsoft.TeamFoundationServer.ExtendedClient.

What about .NET Core?

If you are like me then you are probably pretty excited about .NET Core and the evolution in the way .NET developers build and distribute applications. Like any major platform change however there is always a bit of pain that arises when it comes to making sure all of your dependencies work. If I'm going to use .NET Core on a daily basis for any of the automation that I write then I'm going to need it to work the VSTS/TFS class libraries.

Imagine my sadness then when I launched Visual Studio 2017 and tried to reference the latest versions of the Microsoft.VisualStudio.Services.Client and Microsoft.TeamFoundationServer.Client packages in a .NET Core application and ran into the following error.

PM> Install-Package Microsoft.VisualStudio.Services.Client -pre  
  GET https://api.nuget.org/v3/registration1-gz/microsoft.visualstudio.services.client/index.json
  OK https://api.nuget.org/v3/registration1-gz/microsoft.visualstudio.services.client/index.json 269ms
Restoring packages for C:\Code\Trash\ConsoleApp19\ConsoleApp19\ConsoleApp19.csproj...  
Install-Package : Package Microsoft.AspNet.WebApi.Client 5.2.2 is not compatible with netcoreapp1.1 (.NETCoreApp,Version=v1.1). Package Microsoft.AspNet.WebApi.Client 5.2.2 supports:  
  - net45 (.NETFramework,Version=v4.5)
  - portable-net45+netcore45+wp8+wp81+wpa81 (.NETPortable,Version=v0.0,Profile=wp8+netcore45+net45+wp81+wpa81)
At line:1 char:1  
+ Install-Package Microsoft.VisualStudio.Services.Client -pre
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~<del>
    + CategoryInfo          : NotSpecified: (:) [Install-Package], Exception
    + FullyQualifiedErrorId : NuGetCmdletUnhandledException,NuGet.PackageManagement.PowerShellCmdlets.InstallPackageCommand

Install-Package : Package Microsoft.Net.Http 2.2.22 is not compatible with netcoreapp1.1 (.NETCoreApp,Version=v1.1). Package Microsoft.Net.Http 2.2.22 supports:  
  - monoandroid (MonoAndroid,Version=v0.0)
  - monotouch (MonoTouch,Version=v0.0)
  - net40 (.NETFramework,Version=v4.0)
  - net45 (.NETFramework,Version=v4.5)
  - portable-net40+sl4+win8+wp71+wpa81 (.NETPortable,Version=v0.0,Profile=net40+sl4+win8+wp71+wpa81)
  - portable-net45+win8 (.NETPortable,Version=v0.0,Profile=Profile7)
  - portable-net45+win8+wpa81 (.NETPortable,Version=v0.0,Profile=Profile111)
  - sl4-wp71 (Silverlight,Version=v4.0,Profile=WindowsPhone71)
  - win8 (Windows,Version=v8.0)
  - wpa81 (WindowsPhoneApp,Version=v8.1)
At line:1 char:1  
+ Install-Package Microsoft.VisualStudio.Services.Client -pre
+ </del>~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~<del>
    + CategoryInfo          : NotSpecified: (:) [Install-Package], Exception
    + FullyQualifiedErrorId : NuGetCmdletUnhandledException,NuGet.PackageManagement.PowerShellCmdlets.InstallPackageCommand

Install-Package : Package Newtonsoft.Json 8.0.3 is not compatible with netcoreapp1.1 (.NETCoreApp,Version=v1.1). Package Newtonsoft.Json 8.0.3 supports:  
  - net20 (.NETFramework,Version=v2.0)
  - net35 (.NETFramework,Version=v3.5)
  - net40 (.NETFramework,Version=v4.0)
  - net45 (.NETFramework,Version=v4.5)
  - portable-dnxcore50+net45+win8+wp8+wpa81 (.NETPortable,Version=v0.0,Profile=net45+wp80+win8+wpa81+dnxcore50)
  - portable-net40+sl5+win8+wp8+wpa81 (.NETPortable,Version=v0.0,Profile=Profile328)
At line:1 char:1  
+ Install-Package Microsoft.VisualStudio.Services.Client -pre
+ </del>~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~<del>
    + CategoryInfo          : NotSpecified: (:) [Install-Package], Exception
    + FullyQualifiedErrorId : NuGetCmdletUnhandledException,NuGet.PackageManagement.PowerShellCmdlets.InstallPackageCommand

Install-Package : Package Microsoft.Bcl 1.1.9 is not compatible with netcoreapp1.1 (.NETCoreApp,Version=v1.1). Package Microsoft.Bcl 1.1.9 supports:  
  - monoandroid (MonoAndroid,Version=v0.0)
  - monotouch (MonoTouch,Version=v0.0)
  - net40 (.NETFramework,Version=v4.0)
  - net45 (.NETFramework,Version=v4.5)
  - portable-net40+sl4+win8 (.NETPortable,Version=v0.0,Profile=net40+sl4+win8)
  - portable-net40+sl4+win8+wp71+wpa81 (.NETPortable,Version=v0.0,Profile=net40+sl4+win8+wp71+wpa81)
  - portable-net40+sl4+win8+wp8+wpa81 (.NETPortable,Version=v0.0,Profile=net40+sl4+win8+wp8+wpa81)
  - portable-net40+sl5+win8+wp8+wpa81 (.NETPortable,Version=v0.0,Profile=Profile328)
  - portable-net40+win8 (.NETPortable,Version=v0.0,Profile=Profile5)
  - portable-net40+win8+wp8+wpa81 (.NETPortable,Version=v0.0,Profile=net40+win8+wp8+wpa81)
  - portable-net45+win8+wp8+wpa81 (.NETPortable,Version=v0.0,Profile=Profile259)
  - portable-net45+win8+wpa81 (.NETPortable,Version=v0.0,Profile=Profile111)
  - portable-net451+win81 (.NETPortable,Version=v0.0,Profile=Profile44)
  - portable-net451+win81+wpa81 (.NETPortable,Version=v0.0,Profile=Profile151)
  - portable-win81+wp81+wpa81 (.NETPortable,Version=v0.0,Profile=Profile157)
  - sl4 (Silverlight,Version=v4.0)
  - sl4-wp71 (Silverlight,Version=v4.0,Profile=WindowsPhone71)
  - sl5 (Silverlight,Version=v5.0)
  - win8 (Windows,Version=v8.0)
  - wp8 (WindowsPhone,Version=v8.0)
  - wpa81 (WindowsPhoneApp,Version=v8.1)
At line:1 char:1  
+ Install-Package Microsoft.VisualStudio.Services.Client -pre
+ </del>~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~<del>
    + CategoryInfo          : NotSpecified: (:) [Install-Package], Exception
    + FullyQualifiedErrorId : NuGetCmdletUnhandledException,NuGet.PackageManagement.PowerShellCmdlets.InstallPackageCommand

Install-Package : One or more packages are incompatible with .NETCoreApp,Version=v1.1.  
At line:1 char:1  
+ Install-Package Microsoft.VisualStudio.Services.Client -pre
+ </del>~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~<del>
    + CategoryInfo          : NotSpecified: (:) [Install-Package], Exception
    + FullyQualifiedErrorId : NuGetCmdletUnhandledException,NuGet.PackageManagement.PowerShellCmdlets.InstallPackageCommand

Install-Package : Package restore failed. Rolling back package changes for 'ConsoleApp19'.  
At line:1 char:1  
+ Install-Package Microsoft.VisualStudio.Services.Client -pre
+ </del>~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~<del>
    + CategoryInfo          : NotSpecified: (:) [Install-Package], Exception
    + FullyQualifiedErrorId : NuGetCmdletUnhandledException,NuGet.PackageManagement.PowerShellCmdlets.InstallPackageCommand

Time Elapsed: 00:00:03.8733640  
PM>  

Okaaay, that could have gone better! Let's unpick what just happened here. The first thing to notice is that the package was found just fine:

  GET https://api.nuget.org/v3/registration1-gz/microsoft.visualstudio.services.client/index.json
  OK https://api.nuget.org/v3/registration1-gz/microsoft.visualstudio.services.client/index.json 269ms

If we look down through the log, we come across this gem:

Install-Package : One or more packages are incompatible with .NETCoreApp,Version=v1.1.  
At line:1 char:1  
+ Install-Package Microsoft.VisualStudio.Services.Client -pre
+ </del></del>~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~<del>
    + CategoryInfo          : NotSpecified: (:) [Install-Package], Exception
    + FullyQualifiedErrorId : NuGetCmdletUnhandledException,NuGet.PackageManagement.PowerShellCmdlets.InstallPackageCommand

What this is saying is that this package, or one of its dependencies isn't compatible with .NET Core. Now, .NET finds itself in a great many places these days. For example you've got the classic .NET Framework, you've also got .NET running on Android and iOS devices via Xamarin and UWP and its various forebears - not to mention .NET Core that runs on Windows, Linux and OSX environments for traditional console and web applications.

There have been various approaches taken to try and deal with this spread of execution environments. Some libraries are necessarily platform specific (doesn't make sense to use an Android notification API on a iOS device for example). But some libraries are great candidates for cross platform. One of the ways that we used to deal with this was to create Portable Class Libraries which provided a subset of the programming interface that worked across each of the platforms so that developers could share code (assuming the assemblies themselves were binary compatible). This required the publisher of the component to declare what platforms their library was compatible with (these were called profiles). So if you had a library that worked on a bunch of platforms, it might have a target framework of net40+sl4+win8+wp71+wpa81 meaning that it would work on .NET 4.x, Silverlight 4.x, Windows 8 apps, Windows Phone 7.1 and Windows Phone 8.1. Over time these strings started getting quite long and it was clear that rather than saying you target a particular set of platforms, it would make sense that you target a particular subset of the base class libraries.

.NET Standard Libraries

To solve this problem, the .NET Standard Libraries were born. There are multiple versions of the .NET Standard Library (v1.0, v1.1 and so on) with an expanding subset of the base class libraries. Tools that support developing for particular runtimes (such as .NET Core) know what version of the .NET Standard Library they support and so if a package targets that runtime then it can install it via NuGet. There is a great backgrounder up on the Microsoft Docs site, but the short version is publishing packages against a particular .NET Standard is the future for anything that you want to be portable across runtimes.

Backwards Compatibility

Of course it is one thing to solve this problem moving forward, its another to deal with the practical issue of dealing with all the dependencies that people are already relying on in their applications today. Rather than expect every class library developer to republish their packages there is a hack you can use to let NuGet install a package into a .NET Core application (for example) even when it doesn't target either netcoreapp1.1 directly or a .NET Standard (such as netstandard1.4).

If we look through the logs more closely, you can see excerpts like this:

Install-Package : Package Newtonsoft.Json 8.0.3 is not compatible with netcoreapp1.1 (.NETCoreApp,Version=v1.1). Package Newtonsoft.Json 8.0.3 supports:  
  - net20 (.NETFramework,Version=v2.0)
  - net35 (.NETFramework,Version=v3.5)
  - net40 (.NETFramework,Version=v4.0)
  - net45 (.NETFramework,Version=v4.5)
  - portable-dnxcore50+net45+win8+wp8+wpa81 (.NETPortable,Version=v0.0,Profile=net45+wp80+win8+wpa81+dnxcore50)
  - portable-net40+sl5+win8+wp8+wpa81 (.NETPortable,Version=v0.0,Profile=Profile328)

The Newtonsoft.Json library is a helper library for working with JSON content that you'll find in almost every modern .NET application. The publisher has definately released a version that works against the latest .NET Standard, however the VSTS libraries take dependencies on the PCL based versions of these libraries. As a result when you try to install the Microsoft.VisualStudio.Services.Client package it blows up. The Newtonsoft.Json library isn't the only culprit here - we depend on some other PCLs (even ones owned by Microsoft) that haven't been updated.

To solve this you simply need to specify in your *.csproj file what fallback PCL profile your project supports:

<PropertyGroup>  
    <PackageTargetFallback>portable-net451+win8</PackageTargetFallback>
</PropertyGroup>  

Once you've done that you'll be able to successfully install the packages and things will work just fine. Basically this property says in addition to supporting netcoreapp1.1 and netstandard1.4 you can also use packages that target the portable-net451+win8 profile.

I'm sure that eventually we'll release a version of our wrapper libraries that specifically target .NET Standard, but we've got lots of legacy consumers that don't understand this model. In the meantime this workaround lets you move forward with .NET Core today.

It will be great to see people creating more apps that target .NET Core and of course use the VSTS class libraries for automation!