Assembly Binding Redirect: How and Why

Assembly Binding redirect: How and Why?

Why are binding redirects needed at all? Suppose you have application A that references library B, and also library C of version 1.1.2.5. Library B in turn also references library C, but of version 1.1.1.0. Now we have a conflict, because you cannot load different versions of the same assembly at runtime. To resolve this conflict you might use binding redirect, usually to the new version (but can be to the old too). You do that by adding the following to app.config file of application A, under configuration > runtime > assemblyBinding section (see here for an example of full config file):

<dependentAssembly>
<assemblyIdentity name="C"
publicKeyToken="32ab4ba45e0a69a1"
culture="en-us" />

<bindingRedirect oldVersion="1.1.1.0" newVersion="1.1.2.5" />
</dependentAssembly>

You can also specify a range of versions to map:

<bindingRedirect oldVersion="0.0.0.0-1.1.1.0" newVersion="1.1.2.5" />  

Now library B, which was compiled with reference to C of version 1.1.1.0 will use C of version 1.1.2.5 at runtime. Of course, you better ensure that library C is backwards compatible or this might lead to unexpected results.

You can redirect any versions of libraries, not just major ones.

Why is an assembly binding redirect required for this project?

The .NET runtime has a concept called strong naming.

I'm probably getting lots of technical details wrong, but basically an assembly that is not strongly named effectively says "my name is zivkan.utilities.dll", and when another assembly is compiled against my assembly, the reference says "I need the class named zivkan.utilities.thing from zivkan.utilities.dll". So, it knows nothing about versions and you can drop in any zivkan.utilities.dll that contains a zivkan.utlities.thing class and the runtime will try to run it.

If I strong name sign zivkan.utilities.dll, now the assembles advertises itself as "my name is zivkan.utilites.dll version 1.0.0 with public key ..." (I'm going to leave the public key part out for the rest of my answer). Now, when another assembly is compiled against it, the compiled reference says "I need zivkan.utilities.dll version 1.0.0". Now when this execute, the .NET runtime will only load zivkan.utilities.dll version 1.0.0 and fail if the version is different, like you saw, you get an error. The program can have a binding redirects to tell the .NET runtime assembly loader that when it sees a request for zivkan.utilties between the versions of 0.0.0.0 and 2.0.0.0 to use version 2.0.0.0, which is how you solved your problem.

NuGet versions and assembly versions are two separate concepts, but since they're typically the same value (or a very similar value), it's not so different. Assembly versions are a run-time thing, while NuGet package versions are a build-time thing.

So, imagine the situation without binding redirects. Your program, CommandLineKeyVaultClient is loaded and has a dependency on Newtonsoft.Json version 8.0.2. The .NET runtime loads Newtonsoft.Json.dll and confirms that it is indeed version 8.0.2. Then the .NET runtime sees that CommandLineKeyVaultClient also has a dependency on Microsoft.Rest.ClientRuntime.dll, let's say version 1.0.0.0. So, the .NET runtime loads that dll and confirms the assembly version number. Microsoft.Rest.ClientRuntime.dll has a dependency on Newtonsoft.Json.dll version 6.0.8. The .NET runtime sees that Newtonsoft.Json version 8.0.2 is already loaded, but the version doesn't match and there's no binding redirect, so let's try to load Newtonsoft.Json.dll on disk (there's actually a hook you can use to tell the loader to load the dll from a different directory, when you really need to load different versions of the same assembly, you can). When it tries, it sees the version of the assembly doesn't match the strong named dependency, and fails saying "can't load Newtonsoft.Json.dll version 6.0.8", which is true because the version on disk is actually 8.0.2.

If you use NuGet packages using PackageReference, NuGet will look not only at transitive NuGet dependencies, but also project dependencies and build a graph of all assemblies (project or nuget) that are needed. Then MSBuild should automatically detect when two different assemblies depend on different versions of the same assembly name and generate binding redirects. Therefore, when using PackageReference, this should not generally be a problem.

However, if you use packages.config to define your NuGet dependencies, NuGet will try to add binding redirects when it detects a version conflict (which I think you can opt-out of). But since this is calculated at the time you modifiy NuGet dependencies in that project(install, upgrade or uninstall a package), it's possible to get the binding redirects out of sync, and there's an issue with project to project dependencies and what NuGet packages those project references use.

Anyway, I hope this explains why you get the dll loading error when all your projects have NuGet dependency >= 6.0.8. Again I repeat that assembly versions and NuGet versions are different things, even when they have the same value, and the .NET runtime allows you to load different versions of the same assembly at the same time and needs instructions when you don't want that, which is what binding redirects are.

How to achieve assembly binding redirect in a plugin scenario?

You could deploy D as a publisher policy assembly.

The benefit of this approach is that client directories do not need to contain *.config files to redirect to a newer version. Publisher policy allows the publisher of an assembly to install a binary version of a *.config file into the GAC (along with the assembly). This way the CLR will be able to perform the requested redirection at the level of the GAC.

If you want to bypass the publisher policy for a certain app, you can specify so in the app’s *.config file using the <publisherPolicy> element.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<runtime>
<assemblyBinding xmlns=“urn:schemas-microsoft-com:asm.v1”>
<publisherPolicy apply="no" />
</assemblyBinding>
</runtime>
</configuration>

Eliminating assembly binding redirects when targeting the .Net framework

I decided against trying to replace the config time binding redirects with the runtime assembly resolution. The reasons - I do not know how to ensure it reliably given:

  • Different types of runnable projects - console apps, Asp.Net applications, WCF services, unit test projects. And we have them all.
  • The code may spawn different App Domains and each one has to have this assembly resolution logic. I do not think it is possible at all in general.

Instead I decided to leverage the following aspects of our setup:

  • We already enforce consistent versioning of all the NuGet packages we reference. Migrating to PackageReference has drastically reduced the amount of packages we have control over - hence the numerous problems. But those we directly reference are in order.
  • We now have project.assets.json files which present the entire picture when it comes to NuGet package and project references. We cannot change the transitive dependencies (like we could with packages.config), but we can be aware of all of them.

This makes it possible to write a tool that could read project.assets.json for the given project (and recursively for all the other projects it depends on) and based on them do two things:

  1. Identify all the NuGet package dependencies which are mentioned with different versions. E.g. if NuGet package X depends on NuGet package Y v1, but NuGet package Z depends on Y v2, then Y is problematic. And we can recognize this condition and determine the file path of the highest version - v2.
  2. Update the binding redirects automatically.
  3. After the build copy the files identified in the first step to the published directory.

This way the binaries in the bin folder would not depend on the build order of the projects in the solution and we would have a deterministic process to maintain the binding redirects.

This is a work in progress, but it looks promising - https://github.com/MarkKharitonov/GenerateDotNetBindingRedirects

Runtime assembly binding redirect

After some research, i ended up with this approach

There is a library (https://github.com/0xd4d/dnlib) that lets you edit metadata of assembly and write a new assembly with edited metadata at runtime (and much more).

After loading pluginB, when I need to update library, I am loading a new version of library with Assembly.Load(bytes[]) get its new version, after that with dnlib I change assemblyRef of pluginB to reference to a new version of library, and writing new pluginB assembly to bytes array, then reload it with Assembly.Load(bytes[]), after that pluginB will be using a new version of library.

C# Assembly Binding Redirects - Newtonsoft.Json

The only solution that has an above-average chance of working is for all the libraries to be referencing the same "major" version
of the library (8.*, 9.*, etc - the first number). You should then be able to use assembly bindingly redirects to fix up anything smaller than the "major", although it is increasingly common to see the assembly version effectively pinned at majors, to avoid assembly binding redirect hell.

The key point here is that under semver, any change in the "major" should be considered a breaking change, and thus you should not expect code compiled against a different "major" to work correctly, or even at all.

Note: it is technically possible to use assembly binding redirects across majors; you just shouldn't expect it to actually work. If it does: consider it an unexpected bonus.



Related Topics



Leave a reply



Submit