1

I'm trying to consume Azure DevOps .NET API, specifically the Git client, from Powershell 5.1. There is a copy of all client libraries under C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\CommonExtensions\Microsoft\TeamFoundation\Team Explorer.

So first I tried that in a C# program:

GitHttpClient Cli = new GitHttpClient(new Uri("http://tfs.example.com:8080/tfs/MyCollection/"), new VssCredentials(true));

This line would throw an error that Newtonsoft.Json, v9.0.0.0 was not found. A copy of Newtonsoft.Json.dll is present in the same folder, except its version is 12. I've added an explicit reference to Newtonsoft.Json.dll to the project, rebuilt, and it worked - presumably because the program loads Newtonsoft.Json.dll v12 before AzDevOps client DLLs and the dependency resolution picks that one up despite version mismatch.

Now I'm trying the same in Windows Powershell 5.1 (interactive for now). So first, I'd do

$APIPath = "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\CommonExtensions\Microsoft\TeamFoundation\Team Explorer"
Add-Type -Path "$APIPath\Newtonsoft.Json.dll" 

Then I'd do

Add-Type -Path "$APIPath\Microsoft.TeamFoundation.SourceControl.WebApi.dll"

And that throws an error that Newtonsoft.JSON, v9.0.0.0, or one of its dependencies, can't be found. Why this discrepancy? Wouldn't the previous Add-Type load the DLL into the process and short out the dependency resolution at that, like the C# counterpart does?

Tried constructing a random Newtonsoft object before the second Add-Type to force Newtonsoft DLL loading, under assumption that Add-Type is lazy - same result. The object constructs.

If there is a way to somehow tell Powershell "Newtonsoft 12 is to be used whenever Newtonsoft9 is requested", I'd gladly use that.

UPD: the loaded assembly dump as outlined at https://www.koskila.net/how-to-list-all-of-the-assemblies-loaded-in-a-powershell-session/ confirms that Newtonsoft 12 in loaded. Elsewhere at SO they claim that the first loaded version wins and having multiple versions loaded at the same time is not allowed without some deep magic (multiple AppDomains and such). Yet that's not what I'm seeing.

The loaded assembly list claims that Microsoft.TeamFoundation.SourceControl.WebApi.dll is loaded, but trying to construct the GitHttpClient throws the "can't load Newtonsoft" error.

UPD2: even hairier. So I've located a copy of Newtonsoft 9 on the system, loaded that into Powershell. Now the Add-Type -Path "$APIPath\Microsoft.TeamFoundation.SourceControl.WebApi.dll" line executes, but the GitHttpClient constructor errors our claiming Newtonsoft 6 can't be found. I've poked around with ILSpy, found that:

  • MS.TF.SourceControl.WebApi requires Newtonsoft 9
  • MS.TF.SourceControl.WebApi requires System.Net.Http.Formatting (present in the same API folder)
  • System.Net.Http.Formatting requires Newtonsoft 6

So without whatever magic exists in desktop C# applications and allows upstream dependency resolution, it's just not possible.

UPD3: considered hooking AppDomain.AssemblyResolve, but Powershell (at least v5) can't hook events with return values. Elsewhere they claim that later versions of the assembly should satisfy requirements for earlier ones, but it seems that it only works among major versions. In the AppDomain of the C# application, the AssemblyResolve method doesn't seem to be caught. Could it be driven by AppDomain properties?

Seva Alekseyev
  • 59,826
  • 25
  • 160
  • 281
  • 1
    For your C# app likely a binding redirect was generated at compile time which instructs the runtime to use v12 instead of any previous version. The same does not happen in Powershell. – n0rd Mar 22 '22 at 18:20
  • That's precisely the case; the generated config file contains a `` for Newtonsoft. Now I want to find a way to achieve the same without meddling with machine level configs... :) – Seva Alekseyev Mar 22 '22 at 18:28
  • See https://stackoverflow.com/questions/24181557/powershell-config-assembly-redirect – Seva Alekseyev Mar 22 '22 at 18:51
  • Not entirely sure why you need that git client DLL in the first place. Before diving into assembly resolution tinkering, I would consider using `git.exe` first, then writing a .net app with all required binding redirects present. – n0rd Mar 22 '22 at 19:08
  • Didn't want to add a runtime dependency on the git command line client. Also, I'm not sure if/how git.exe supports the current Windows credentials, the way TFS REST API does. – Seva Alekseyev Mar 22 '22 at 19:59

1 Answers1

1

Preamble: the dependency resolution in the C# program was not picking up v12 where v9/6 was requested automagically; it was only doing so because the config file of the compiled program was telling it so, and that only happened once and because Newtonsoft v12 was being referenced in the project. Thanks to @n0rd for pointing that out. Resolving strongly named dependent assemblies to a higher major version is not a default behavior in .NET 4.5-8.

Modifying the config of Powershell to achieve the same might be possible, but I didn't go there. The original piece that needed this logic will be eventually running on servers that I don't control, so the less administrative overhead, the better. Now, for the working answer.


You can provide a resolve handler in Powershell 5 after all, telling .NET to use the loaded version of Newtonsoft in lieu of any other one. It goes like this:

$OnAssemblyResolve = [ResolveEventHandler] {
    param($o, $e)
    if($e.Name.StartsWith("Newtonsoft.Json,"))
    {
        return [AppDomain]::CurrentDomain.GetAssemblies() | ?{$_.FullName.StartsWith("Newtonsoft.Json,")}
    }
    return $null
}

Add-Type -Path "$APIPath\Newtonsoft.Json.dll"
[AppDomain]::CurrentDomain.add_AssemblyResolve($OnAssemblyResolve)
Add-Type -Path "$APIPath\Microsoft.TeamFoundation.SourceControl.WebApi.dll"
Add-Type -Path "$APIPath\Microsoft.VisualStudio.Services.WebApi.dll"
Add-Type -Path "$APIPath\System.Net.Http.Formatting.dll"
[AppDomain]::CurrentDomain.remove_AssemblyResolve($OnAssemblyResolve)

Once done loading Newtonsoft dependent assemblies (notably System.Net.Http.Formatting), remove the handler. Otherwise, it may interfere with Powershell's own functioning and cause a stack overflow exception, where an "assembly not found" condition within Powershell triggers the handler, which requires the same assembly to run, causing an endless recursion. In my case it happened downstream, when the script was trying to throw an unrelated exception, which required that System.Management.Automation.resources is loaded, which was not found, etc.


My previous statement that Powershell 5 couldn't hook .NET events with return values was wrong. I vaguely recall reading the docs for some event handling cmdlet, which mentioned that returning values from the handler block was not supported, guess that's where this misconception of mine came from.

Seva Alekseyev
  • 59,826
  • 25
  • 160
  • 281
  • 1
    Consider being a bit more specific with assembly name, [there are](https://www.nuget.org/profiles/jamesnk) several ones that start with `Newtonsoft.Json`. – n0rd Mar 22 '22 at 19:08
  • 1
    The trailing comma in strong name will address that. – Seva Alekseyev Apr 01 '22 at 13:49