dotnet / dotnet/docs

[docs] Overview of System.Net.Http assembly binding redirect behavior: net framework 4.x with netstandard2.0 dependency

Open
#46,894 2 comments 0 reactions 0 assignees View on GitHub
:watch: Not Triaged
Dominant language
No language data
Stars
4.8k
Forks
6.1k
Avg merge
15h 21m
Merged PRs (30d)
370

Description

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 `` 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](https://github.com/dotnet/standard/blob/release/2.0.0/docs/netstandard-20/README.md#net-framework-461-supporting-net-standard-20)).

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](https://github.com/user-attachments/assets/17801998-8f40-48e1-97eb-8b6f737694af)

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.DLL`s 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>
```

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.