Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Overview of System.Net.Http assembly binding redirect behavior: net framework 4.x with netstandard2.0 dependency #109851

Open
matt961 opened this issue Nov 14, 2024 · 1 comment
Labels
area-System.Net.Http untriaged New issue has not been triaged by the area owner

Comments

@matt961
Copy link

matt961 commented Nov 14, 2024

Just wanting to put this information somewhere to consolidate some research around the issue and what I found to be a sane solution as of 2024-11-14 in hopes that it helps other people in the same situation trying to develop netstandard2.0 packages to be consumed by both .NET Framework 4 and .NET SDK applications, with the newer tooling.

I realize that this problem is old and .NET Framework 4.6.1 is unsupported, but the lack of consolidated documentation about this subtle behaviour has cost me and many others a lot of headache, so I'm hoping to fill that gap with this issue.

Environment

  • VS 2022 Professional (17.10.5)
  • VS 2019 Build Tools (required to work with .NET Framework 4.6.1)
  • Windows 10 w/ .NET Framework 4.8.1 installed (codenum 533325 == 4.8.1)
    • Get installation codenum with Get-ItemPropertyValue -LiteralPath 'HKLM:SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full' -Name Release
  • .NET Framework 4.6.1 web project with a netstandard2.0 dependency

Problem

With the introduction of netstandard2.0, some workarounds were introduced to assembly resolution process of MSBuild / Visual Studio which can sometimes not play nicely with dependencies that depend on the System.Net.Http package versions currently on NuGet, which are now deprecated, because netstandard2.0 ships with version 4.2.0.0, and .NET Framework 4.6.1 is declared compatible retroactively. So, some older .NET Framework installs may not have this System.Net.Http 4.2.0.0 version available, causing failures at runtime due to missing API surface.

Assembly binding redirect management seems to be the crux of this issue, where the version range generated can sometimes be incorrect (at least for me and a few of our devs, it was).

Solution

TL;DR

  • Ensure your Windows hosting environment is running NET Framework 4.8+
  • Ensure project(s) in your solution depend on System.Net.Http without
    specifying a version:

    in .csproj <Reference Include="System.Net.Http" /> and ensure no other
    previous references to System.Net.Http are present

  • Ensure no binding redirect is present in Web.config / App.config for
    System.Net.Http.

This way, all projects rely on the netstandard2.0 standardization of
System.Net.Http, which is backwards compatible and should not break
anything.

After fixing the references to System.Net.Http and reloading solution,
Update-Package -Reinstall and/or Add-BindingRedirect in VS Package Manager
Console should remove System.Net.Http binding redirect entirely.

I noticed that to fix the issue, a full reload of a solution in VS was
required in order for Add-BindingRedirect to correctly remove assembly
redirect for System.Net.Http in our web projects after changes to references
were made.

Issue #29314 has the most relevant information out of any issue discussion that I have discovered and eventually led me to this solution.

After having a few previous run-ins with various System.Net.Http assembly versions not being found at runtime due to incorrect binding redirects, I found myself facing a new issue once updating a dependency of a web project to target netstandard2.0. The resolved dependency in References view of solution explorer switched from NuGet package (pkg version 4.3.4 but really, assembly version 4.1.1.3) to assembly version 4.2.0.0 which resolved to a path under VIsual Studio 2022 installation.

I assume that the default behaviour to not simply resolve 4.2.0.0 regardless of having a netstandard2.0 dependency was to not break deployments of 4.6.1 applications to Windows environments that did not have up to date .NET Framework installations with netstandard2.0 backwards compatibility patches to 4.6.1 runtime, as System.Net.Http 4.2.0.0 is part of netstandard2.0 which was released much later (for context, .NET Framework 4.6.1 supporting .NET Standard 2.0).

So, this solution assumes you are deploying to an environment with .NET Framework >= 4.8 (possibly 4.7.1 works? hard to tell since there were some fixes between 4.7.1 and 4.7.2 related to some missing stabilized netstandard2.0 API surface). 4.8 is available in patches to now very old Windows releases (e.g. Server 2012) so for web hosting, this should not be an issue as you can keep the same targetFramework and still run latest install in hosting environment (e.g. I deploy 4.6.1 target for web projects but hosting environments (Windows Server, Azure App Service) have 4.8 installed).

In order to verify that binding redirects were not required, I used fuslogvw command by running VS as administrator and opening Tools > Command Line > Developer Powershell, running fuslogvw and changing Settings > Log all binds to disk.

Debugging the project with (left) and without (right) the binding redirect to 4.2.0.0, which was added manually for this test since I have binding redirects generating correctly now, I captured the following assembly loading diagnostics and added some added annotations and in-lined investigation:
Image

Even though the redirect was working according to fuslogvw, the same assembly version 4.0.0.0 is being loaded, with the only obvious difference being that not having the redirect causes the assembly to be loaded from GAC (Global Assembly Cache). I attribute this behavior to work of the NET Framework 4.6.1 and netstandard2.0 retroactive compat layer magic.

To confirm that the two successfully bound DLL files were in fact the same assembly, I compared them all in powershell, and additionally, to the one copied to bin/ folder on build, and an unrelated assembly to make sure the .Equals was working as expected:

PS C:\Windows\...> $a = ([system.reflection.assembly]::loadfile((get-item "C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System.Net.Http\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Net.Http.dll").FullName))
PS C:\Windows\...> $b = ([system.reflection.assembly]::loadfile((get-item "C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files\vs\248979d4\9b699876\assembly\dl3\43dbadef\50376cf4_05e2da01\System.Net.Http.dll").FullName))
PS C:\Windows\...> $c =  ([system.reflection.assembly]::loadfile("C:\Users\MyUsername\projects\web\API\app\API\RedactedProjectName\bin\System.Net.Http.dll")
>> )
PS C:\Windows\...> $d =  ([system.reflection.assembly]::loadfile("C:\Users\MyUsername\projects\web\API\app\API\RedactedProjectName\bin\System.Net.Security.dll")
>> )
PS C:\Windows\...> $a.Equals($b)
True
PS C:\Windows\...> $b.Equals($c)
True
PS C:\Windows\...> $c.Equals($d)
False

All three System.Net.Http.DLLs are the exact same assembly still reporting version 4.0.0.0.

So, is it using 4.2.0.0 but reporting as 4.0.0.0 in all cases?

Conclusion

The results are that System.Net.Http continues to load properly and work correctly, given that:

  • the web project at framework 4.6.1, which reports reference to 4.2.0.0 in VS due to a netstandard2.0 package being referenced
  • an additional class lib project at framework 4.6.1, which the web project depends on, reports a reference to version 4.0.0.0 in VS (no netstandard2.0 dependency in this project)
  • no binding redirect required for System.Net.Http

Appendix

fuslogvw output (with redactions)

With binding redirect to 4.2.0.0 added manually:

*** Assembly Binder Log Entry  (2024-11-13 @ 8:09:25 PM) ***

The operation was successful.
Bind result: hr = 0x0. The operation completed successfully.

Assembly manager loaded from:  C:\Windows\Microsoft.NET\Framework64\v4.0.30319\clr.dll
Running under executable  C:\Program Files\IIS Express\iisexpress.exe
--- A detailed error log follows. 

=== Pre-bind state information ===
LOG: DisplayName = System.Net.Http
 (Partial)
WRN: Partial binding information was supplied for an assembly:
WRN: Assembly Name: System.Net.Http | Domain ID: 2
WRN: A partial bind occurs when only part of the assembly display name is provided.
WRN: This might result in the binder loading an incorrect assembly.
WRN: It is recommended to provide a fully specified textual identity for the assembly,
WRN: that consists of the simple name, version, culture, and public key token.
WRN: See whitepaper http://go.microsoft.com/fwlink/?LinkId=109270 for more information and common solutions to this issue.
LOG: Appbase = file:///C:/Users/MyUsername/projects/web/API/app/API/RedactedProjectName/
LOG: Initial PrivatePath = C:\Users\MyUsername\projects\web\API\app\API\RedactedProjectName\bin
LOG: Dynamic Base = C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files\vs\248979d4
LOG: Cache Base = C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files\vs\248979d4
LOG: AppName = 9b699876
Calling assembly : (Unknown).
===
LOG: This bind starts in default load context.
LOG: Using application configuration file: C:\Users\MyUsername\projects\web\API\app\API\RedactedProjectName\web.config
LOG: Using host configuration file: C:\Users\MyUsername\Documents\IISExpress\config\aspnet.config
LOG: Using machine configuration file from C:\Windows\Microsoft.NET\Framework64\v4.0.30319\config\machine.config.
LOG: Policy not being applied to reference at this time (private, custom, partial, or location-based assembly bind).
LOG: Attempting download of new URL file:///C:/Windows/Microsoft.NET/Framework64/v4.0.30319/Temporary ASP.NET Files/vs/248979d4/9b699876/System.Net.Http.DLL.
LOG: Attempting download of new URL file:///C:/Windows/Microsoft.NET/Framework64/v4.0.30319/Temporary ASP.NET Files/vs/248979d4/9b699876/System.Net.Http/System.Net.Http.DLL.
LOG: Attempting download of new URL file:///C:/Users/MyUsername/projects/web/API/app/API/RedactedProjectName/bin/System.Net.Http.DLL.
LOG: Assembly download was successful. Attempting setup of file: C:\Users\MyUsername\projects\web\API\app\API\RedactedProjectName\bin\System.Net.Http.dll
LOG: Entering download cache setup phase.
LOG: Assembly Name is: System.Net.Http, Version=4.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
LOG: A partially-specified assembly bind succeeded from the application directory. Need to re-apply policy.
LOG: Using application configuration file: C:\Users\MyUsername\projects\web\API\app\API\RedactedProjectName\web.config
LOG: Using host configuration file: C:\Users\MyUsername\Documents\IISExpress\config\aspnet.config
LOG: Using machine configuration file from C:\Windows\Microsoft.NET\Framework64\v4.0.30319\config\machine.config.
LOG: Redirect found in application configuration file: 4.2.0.0 redirected to 4.2.0.0.
LOG: Post-policy reference: System.Net.Http, Version=4.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
LOG: Binding succeeds. Returns assembly from C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files\vs\248979d4\9b699876\assembly\dl3\43dbadef\50376cf4_05e2da01\System.Net.Http.dll.
LOG: Assembly is loaded in default load context.

...

PS C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files\vs\248979d4\9b699876\assembly\dl3\43dbadef\50376cf4_05e2da01> ([system.reflection.assembly]::loadfile((get-item "C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files\vs\248979d4\9b699876\assembly\dl3\43dbadef\50376cf4_05e2da01\System.Net.Http.dll").FullName)).FullName

System.Net.Http, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a

No binding redirect for System.Net.Http:

*** Assembly Binder Log Entry  (2024-11-13 @ 8:11:41 PM) ***

The operation was successful.
Bind result: hr = 0x0. The operation completed successfully.

Assembly manager loaded from:  C:\Windows\Microsoft.NET\Framework64\v4.0.30319\clr.dll
Running under executable  C:\Program Files\IIS Express\iisexpress.exe
--- A detailed error log follows. 

=== Pre-bind state information ===
LOG: DisplayName = System.Net.Http
 (Partial)
WRN: Partial binding information was supplied for an assembly:
WRN: Assembly Name: System.Net.Http | Domain ID: 2
WRN: A partial bind occurs when only part of the assembly display name is provided.
WRN: This might result in the binder loading an incorrect assembly.
WRN: It is recommended to provide a fully specified textual identity for the assembly,
WRN: that consists of the simple name, version, culture, and public key token.
WRN: See whitepaper http://go.microsoft.com/fwlink/?LinkId=109270 for more information and common solutions to this issue.
LOG: Appbase = file:///C:/Users/MyUsername/projects/web/API/app/API/RedactedProjectName/
LOG: Initial PrivatePath = C:\Users\MyUsername\projects\web\API\app\API\RedactedProjectName\bin
LOG: Dynamic Base = C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files\vs\248979d4
LOG: Cache Base = C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files\vs\248979d4
LOG: AppName = 9b699876
Calling assembly : (Unknown).
===
LOG: This bind starts in default load context.
LOG: Using application configuration file: C:\Users\MyUsername\projects\web\API\app\API\RedactedProjectName\web.config
LOG: Using host configuration file: C:\Users\MyUsername\Documents\IISExpress\config\aspnet.config
LOG: Using machine configuration file from C:\Windows\Microsoft.NET\Framework64\v4.0.30319\config\machine.config.
LOG: Policy not being applied to reference at this time (private, custom, partial, or location-based assembly bind).
LOG: Attempting download of new URL file:///C:/Windows/Microsoft.NET/Framework64/v4.0.30319/Temporary ASP.NET Files/vs/248979d4/9b699876/System.Net.Http.DLL.
LOG: Attempting download of new URL file:///C:/Windows/Microsoft.NET/Framework64/v4.0.30319/Temporary ASP.NET Files/vs/248979d4/9b699876/System.Net.Http/System.Net.Http.DLL.
LOG: Attempting download of new URL file:///C:/Users/MyUsername/projects/web/API/app/API/RedactedProjectName/bin/System.Net.Http.DLL.
LOG: Assembly download was successful. Attempting setup of file: C:\Users\MyUsername\projects\web\API\app\API\RedactedProjectName\bin\System.Net.Http.dll
LOG: Entering download cache setup phase.
LOG: Assembly Name is: System.Net.Http, Version=4.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
LOG: A partially-specified assembly bind succeeded from the application directory. Need to re-apply policy.
LOG: Using application configuration file: C:\Users\MyUsername\projects\web\API\app\API\RedactedProjectName\web.config
LOG: Using host configuration file: C:\Users\MyUsername\Documents\IISExpress\config\aspnet.config
LOG: Using machine configuration file from C:\Windows\Microsoft.NET\Framework64\v4.0.30319\config\machine.config.
LOG: Version redirect found in framework config: 4.2.0.0 redirected to 4.0.0.0.
LOG: Post-policy reference: System.Net.Http, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
LOG: Binding succeeds. Returns assembly from C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System.Net.Http\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Net.Http.dll.
LOG: Assembly is loaded in default load context.


...
somehow its the same assembly version???


PS C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files\vs\248979d4\9b699876\assembly\dl3\43dbadef\50376cf4_05e2da01> ([system.reflection.assembly]::loadfile((get-item "C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System.Net.Http\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Net.Http.dll").FullName)).FullName

System.Net.Http, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
PS C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files\vs\248979d4\9b699876\assembly\dl3\43dbadef\50376cf4_05e2da01>
@dotnet-policy-service dotnet-policy-service bot added the untriaged New issue has not been triaged by the area owner label Nov 14, 2024
Copy link
Contributor

Tagging subscribers to this area: @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-System.Net.Http untriaged New issue has not been triaged by the area owner
Projects
None yet
Development

No branches or pull requests

1 participant