I’m going to show my ignorance here for a second. Don’t get me wrong, its always there, I just don’t always show it.
I didn’t understanding what assembly binding redirection did until yesterday. I mean, I always saw app.config files being added/modified whenever I added a new package through NuGet, and it piqued my interest, but I never really had a reason to investigate further. It didn’t seem to be hurting (apart from some increased complexity in the config file) so I left it to do whatever the hell it was doing.
Yesterday (and the day before) I went through a solution with 58 projects with the intent of updating all of the Ninject references to the latest version. I’d added a reference to the Ninject.MockingKernel.Moq package to a test project
I know what you might be thinking, 58 projects! Yes I know that’s a lot. Its a bigger problem than I can solve right now though. Its on my list.
I know the second thing you might be thinking too. How many references to Ninject were there to update? A well factored application would only have 1 reference, in the entry point (or Composition Root). This particular solution had 4 entry points (3 web applications and a service), but references to Ninject were riddled throughout the rest of the projects as well, for a number of reasons:
- Usage of the Service Locator anti-pattern, which exposed a static IKernel to, well, anything and everything.
- Usage of Ninject attributes (property injection, marking which constructor to use for the kernel, etc).
- Some projects had NinjectModules that mapped the interfaces in those projects to the concrete implementation (also in those projects).
IoC (and in particular Ninject) are amazing for a variety of reasons I won’t go into here, but its very easy to do it poorly.
Normally this would be a fairly painless process, just go into the NuGet package manager, check the Updates section, select Ninject and hit update. The problem was, not all of the references to Ninject had been added through NuGet. Some were hard references to a version of Ninject that had been downloaded and placed in a lib folder. Other references to Ninject had been added automatically as a result of adding NuGet packages with Ninject dependencies (like Agatha).
Of course, I didn’t realise that some of the references had been manually added, so I naively just used NuGet to update all the references it did know about, compiled, and then did a quick smoke test on the web applications.
Could not load file or assembly ‘Ninject, Version=220.127.116.11, Culture=neutral, PublicKeyToken=c7192dc5380945e7’ or one of its dependencies. The located assembly’s manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040).
Fair enough, that seems straightforward. Something is trying to load the old version and its going pear-shaped because the only Ninject.dll in the output directory is version 18.104.22.168. I went through the projects with a fine tooth comb, discovered that not all of the references had been updated to the latest version (and that some weren’t even using NuGet), fixed all that and tried again.
Still the same error.
I was sure that I had caught all the references, so I search through all of the .csproj files for every reference to 22.214.171.124 and couldn’t find any.
If you’re familiar with binding redirect, you can probably guess the thing that I did that I left out.
When I did the upgrade/installation, I was very wary of unintended changes to other parts of the system. One of the things that happened as a result of installing/updating through NuGet was the editing or addition of many app.config files for the projects in the solution. Specifically, the addition of the following chunk of XML to every project using Ninject.
<dependentAssembly> <assemblyIdentity name="Ninject" publicKeyToken="c7192dc53809457" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-126.96.36.199" newVersion="188.8.131.52" /> </dependentAssembly>
Here’s where the ignorance I mentioned earlier shows up. I thought that since I wasn’t going to be using version 184.108.40.206 of Ninject anywhere, I could safely ignore those changes, so I removed them.
After an embarrassing amount of time spent yelling at my development environment and searching the internet (“Could not load X” errors are surprisingly common) I finally realised that it was my actions that caused my issue.
I was right. None of my assemblies were using Ninject 220.127.116.11. However, the Agatha.Ninject.dll assembly did. Having no control over that assembly, I couldn’t upgrade its reference. Of course, NuGet had already thought of this and helpfully solved the problem for me…..until I just ignored its suggested configuration.
A bindingRedirect entry in an app.config file forces all assembly bindings for that assembly to redirect to the version specified. This works not just for dll’s that you have control over (i.e. your entry point and your projects) but also every assembly that you load and their dependencies as well.
Restoring the bindingRedirects for the entry points (the 3 web applications and the service) fixed the issue. I left it out of the rest of the projects because it seems like the sort of thing you want to set only at the entry point (kind of like IoC container configuration).
So in summary, never assume something exists for no reason, assume you just don’t understand the reason yet.