Gone Girl

I ran into a weird problem this week. Long story short: Pure code changes in startup Project 1), without touching the project reference/or binding redirect at all, ended up a completely different start-up project 2) gets crashed following my changes:

System.IO.FileNotFoundException: ‘Could not load file or assembly ‘Microsoft.Extensions.Configuration, Version=3.1.7.0, > Culture=neutral, PublicKeyToken=adb9793829ddae60’ or one of its dependencies. The system cannot find the file specified.’

Inspected the bin of startup project 2) and found - Microsoft.Extensions.Configurations.dll is…GONE!

Investigation leads into two indpendent issues:
a) the startup project 2) is incorrectly referencing the startup project 1) was working on - so making a change in startup project 1) could have a side effect on the other startup project I did not intend to touch.

At this point, the obvious solution seems to be removing the undesirable dependency of porject 1) from project 2), but wait - what if it is a CORRECT dependency, what if the changes I made was in a lib as opposed to a start-up project? so that leads to a very interesting questions thereafter:

b) How could a line of code change make an assemly dll missing from the bin?

I managed to replicate this issue in a small app:

https://github.com/lnhzd/lnhzd.assemblies.playground

Startup Project:
|-LibA is referencing Microsoft.Extensions.Configurations 2.1.0
  |-LibB is referencing Microsoft.Extensions.Configurations 3.1.7
  |-LibC is referencing Microsoft.Extensions.Configurations 2.1.0

Assembly binding in startup project redirects everything to 2.1.1.0:

  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.Extensions.Configuration" publicKeyToken="adb9793829ddae60" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-2.1.1.0" newVersion="2.1.1.0" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>

LibC is the only project that TRUELY needs this dependency (Microsoft.Extensions.Configuration) - TRUELY means it actually reference a type DEFINED in Microsoft.Extensions.Configuration assembly. Others are just redundant reference that compiler will usually ignore during build.

and…the magic begins by changing a line in LibB,

            // uncomment the code below and you'll see Microsoft.Extensions.Options.dll
            // missing from output folder for the startup project
            // var config = new ConfigurationBuilder();

Uncommenting this line of code means: Microsoft.Extenions.Configuration then becomes a TURELY needed reference. this is the point msbuild starts considering pulling int o assembly of version 3.1.7, but due to the incorrect (legacy) binding redirect in the startup project - msbuild cannot unify the correct version to be copied into startup bin - hence decided NOT to copy it to bin at all. Prior to this line of change, although it looks like multipe version of Microsoft.Extensions.Configurations package are refrenced, the fact is only a single version is refernced during build (2.1.1.0) - this has been changed since a phantom reference becomes a real reference.

What I have learnt from this issue: It is not only runtime makes use of binding redirect, msbuild makes use of it as well.
A line of code change (e.g. by reference a type in an assembly) can make this reference change from an insignificant reference to a significant refernece, hence may affect the unifying process during build - I’ve been aware of this in the past but think it’s also handy to record it here.

Solution: I ended up change the binding redirect in startup project 2), in order for MS to pick up the correct assembly and copy it into bin. Obviously there are other solutions, e.g.: explicitly install this nuget in a start up project - but since I’m dealing with nearly 20 start-up projects I’d rather make this package installed in one lib, then auto copied into all the startup projects.

Written on August 29, 2020