In .Net full framework, when using strong naming, handling of different versions of dependencies has been tedious.
Until somewhat recently, that is. Today, we can slap a
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects> into our csprojs (or better in a
Directory.Build.props file), job done, right ? Well, not quite…
A first small hurdle comes from library projects. By default, there won’t be a
.config file generated, since usually only the main
.exe entry point’s config file is used at runtime. However, in some hosting scenarios, a class library may be loaded in a dedicated AppDomain, where the corresponding
.dll.config will be injected. This is often the case for test runners and the corresponding test assemblies, for instance. However this is just another property :
What about libraries used as plugins ? For now, I have no silver bullet there. If the entry point has no idea of the dependencies that may run in the final process, there can be no appropriate binding redirect generated. Here are a couple of possible mitigations :
- Similarly as what some test runners do, have the plugin code run in a dedicated
AppDomain, where its own
.dll.configfile can be used. However this seriously limits the possibilities for seamless integration, with the need for serialization and/or
- Sometimes you want to write code mostly against interfaces, but at the topmost level, the set of implementations is well known. In this case, this topmost entry points can have very little code, but be used mostly for packaging, with references to all assemblies needed at runtime. Then these entry points will have correct binding redirects.
A weirder corner case
Imagine that you want to deliver some code as a (private) NuGet package. You have a rather small API surface, but a much larger volume of code for the implementation(s). So you package only a handful of dlls in the
lib part of the package, with relevant interfaces and factories. At runtime these dlls (at least the factories) use a bunch of other dlls, which are copied into the final output directory by a specific
<Target> specified in the
build/<package>.targets file of the package. How well will binding redirects be handled in this case ?
TLDR; It’s complicated, some things may work by chance, with a small change breaking stuff…
Old style projects
packages.config project, though I didn’t check (we use
PackageReference only in projects migrated to SDK-style). Then things are rather simple : all directly referenced assemblies are checked, then their references, and so on. The referenced assemblies are searched in a few location, including the output directory. Of course, as soon as a reference is not found, lookup stops there with a message from the compiler about potential issues (if msbuild log is verbose enough). Therefore, after a first build, as long as things have a chance to work at runtime, all transitive dependencies are present, and all binding redirects will be properly generated.
In this case, the output directory is not used for lookup : since transitive references and dependencies is the rule, everything is supposed to be found just by recursively following explicitly stated dependencies (including content of the proper
lib/ folders of NuGet packages). Our use-case is broken in this case : the dlls that are copied are not found, and there own dependencies are not considered when generating binding redirects.
Things may still work by chance if other “normal” references have a conflict with the same dependency : then as long as we end up with the latest version, the generated binding redirect is OK, even if the missed conflict would be with another (older) version.
There is another very strange situation (which actually happens for us) where things may work : by default
<None> items that happen to be assemblies are considered when looking up dependencies. The default
<None> items include all files within the project (
<None Include="**/*" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" /> in
Microsoft.NET.Sdk.DefaultItems.props), except that without surprise,
DefaultItemExcludes excludes the output directories. However, in our setting, most non-toplevel assemblies use the same output directory, which is defined by defining
BaseOutputPath in a
Directory.Build.props file. Then, only for the top-level assemblies (those for which we actually want binding redirects), we override
OutDir to the
bin directory next to the csproj. This is where the twist is : there is a bug in MSBuild, which makes the
OutDir as we define it, with a leading
.\, to be ignored. So at the end, in this case, all the
bin directory ends up included in the
<None> items, and considered as a potential reference. So here again, after a previous build has copied the dlls, further compilations will consider them, and the binding redirects will be correctly generated. Needless to say, we don’t want to rely on this !
Binding redirects in
.config files should be mostly a thing of the past. However, until we move fully to .Net Core, or at least ditch strong naming, they will be needed. Generating them automatically is often fine, but sometimes not enough : rigorous testing is needed to identify edge cases. And in these cases, it helps to understand how things really work, so that we can find efficient workarounds.