'Can't use GitHttpClient from Powershell, Newtonsoft.Json version conflict
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?
Solution 1:[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.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 |