当单个依赖程序集,比如A,依赖程序集B,比如使用B的1.0版本。现在B从1.0升级到了2.0。那么此时可以在程序集A中手动重定向B的版本,这样在程序集A运行后会自动使用更新的2.0版本的程序集B。
程序集的版本重定向须写在程序集自己的配置文件中(App.config)。在<configuration> – <runtime> – <assemblyBinding> – <dependentAssembly> 元素下。首先必须需要一个<assemblyIdentity>元素来指定依赖的(引用)程序集。包括它的名称,公钥标记等……。接着就是<bindingRedirect>元素代表重定向信息,使用<bindingRedirect>的oldVersion和newVersion属性来指定新旧版本的信息。
还是上面的实例,如果用标题1的方法的话,每一个引用B旧版本的程序集,都需要手动配置自己的把依赖的版本1的B程序集重定向成版本2的B程序集。显然不是个好的方法。.NET还提供发行者策略(Publisher Policy)。其实就是把针对某个程序集的配置直接跟随程序集编译,然后在部署时,程序集和发行者策略都会存储在GAC里,这样任何引用该程序集的对象都会受到影响,而不用一个一个慢慢配置了。
创建开发者策略程序集需要使用al.exe的/link参数来制定一个发行者策略文件。具体可以参考MSDN:http://msdn.microsoft.com/zh-cn/library/dz32563a.aspx
接下来还有一个问题,既然发行者策略程序集创建成功,那么所有使用它的程序集都会根据发行者策略而受到影响,某些时候程序集不需要受到发行者策略影响。则需要在<assemblyBinding>元素下加入,<publisherPolicy>元素,用apply属性来控制是否应用发行者策略配置。
如下代码:
第三个情景就是在程序集的开发测试中,往往频繁更新版本,此时则需要设置发行者策略配置并将程序集安装到GAC内。那么可以将程序集所在目录设置到Windows环境变量DEVPATH中。然后在machine.config(注意是machine.config,不是app.config)中打开开发模式(通过<developmentMode>节点),这样CLR在寻找程序集时不仅要浏览GAC,还是探索DEVPATH中的目录,程序集就不必总安装到GAC中了。
进行插件式编程的时候,经常性地弹出这么个东西找到的程序集清单定义与程序集引用不匹配。 (异常来自 HRESULT:0x80131040),往往这种问题特别难以解决,搞定了一个还要出另外一个。得研究一下怎么处理。

引用不匹配
这里提示需要加载一个4.2.0.0版本的dll,我先看看文件夹下面有没有对应的dll,查看文件dll的详细信息。

这个版本号4.6.27818.1和4.2.0.0也差的有点太远了吧,是这个问题?其实不是的,这个地方显示的版本和程序集的版本不是一回事。
程序集版本
.NET程序有很多版本的说法,官方对这个有解释,通过文件管理器获得的版本是AssemblyFileVersion
,而程序集加载器定位的版本使用的是AssemblyVersion
,这两个东西完全不是一回事。通过右键,我们看不到AssemblyVersion,比较简单的方式,可以通过Powershell脚本来查看程序集版本。
ls *.dll -r |
ForEach-Object {
try {
$_ | Add-Member NoteProperty FileVersion ($_.VersionInfo.FileVersion)
$_ | Add-Member NoteProperty AssemblyVersion (
[Reflection.AssemblyName]::GetAssemblyName($_.FullName).Version
)
} catch {}
$_
} |
Select-Object Name,FileVersion,AssemblyVersion
可以看到,我这边的程序集是4.2.0.1版本的,不是4.2.0.0版本的,因此,程序集不能正常加载。

我自己的项目是使用nuget进行包管理的,引用的包的版本号是4.5.3(又多一个版本...),程序集版本是4.2.0.1。我找遍了整个项目,都没有找到我dll项目中关于4.2.0.0版本的引用,苦思良久,打盹的时候忽然想起来,是不是那个exe的问题?
绑定重定向
我的主exe程序是使用.NET Framework 4.6.1进行编译,然后单独编译dll作为插件放入一个文件夹,由exe程序进行加载。那有可能是exe引用了4.2.0.0这个版本,或者是其他dll插件引用了这个版本,造成版本不兼容。
这种情况可以有很多种解决方案,这篇文章写的非常详细,推荐读一读。而我这里使用了最简单也是作者比较推荐的办法,程序集引用的绑定重定向。
使用这个方法有一个前提,你需要完全了解其他程序引用这个程序版本的时候不会出问题,一般小版本号的变动,是相对比较安全的。
一直以来,我写.NET Framework程序貌似就很少有这种问题,是因为微软会自动给程序设置重定向,我们可以通过在项目上右键,点选自动生成绑定重定向。

之后,会出现一个app.config
文件,它会在生成程序的时候,变成程序名称.dll.config
的形式,里面大概是这个样子的:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Threading.Tasks.Extensions" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.2.0.1" newVersion="4.2.0.1" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
可以发现,这个东西对我们引用的版本内容进行了限制,强制某个范围内的版本重定向为一个固定的版本号。不过,虽然dll中已经有这个内容,但是exe不会理会它的,它只会看自己主程序的.exe.config文件。所以我们需要做的,就是把这一个部分内容搬到主程序的.exe.config
中。
修改后重新运行,然后有又提示找不到System.Buffers.dll了,如法炮制,问题解决。
Fusion Log
有时候,通过这个方法找下去,处理完了,也不一定能够解决,因为这个错误提示的是“未能加载文件或程序集或它的某一个依赖项。”,也可能是某个引用的依赖项出了问题,这样就不是很好找了。
好在.NET提供了一个程序集绑定日志的工具,可以帮助我们查看绑定的问题。一般情况下,这个东西是关闭的,系统会提示:
警告: 程序集绑定日志记录被关闭。
要启用程序集绑定失败日志记录,请将注册表值 [HKLM\Software\Microsoft\Fusion!EnableLog] (DWORD)设置为 1。
注意: 会有一些与程序集绑定失败日志记录关联的性能损失。
要关闭此功能,请移除注册表值 [HKLM\Software\Microsoft\Fusion!EnableLog]。
调试的话,可以打开这个注册表键值。或者简单点,直接使用Fuslogvw.exe
(程序集绑定日志查看器),用管理员账号启动,在设置中设置好记录的日志信息,然后就可以记录了,详细的使用方法,见这里。就能看到详细的信息了,可以帮助我们深入分析内部绑定的问题。(用完记得关,有性能损失。)
=== 预绑定状态信息 ===
日志: DisplayName = System.Threading.Tasks.Extensions, Version=4.2.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
(Fully-specified)
日志: Appbase = file:///C:/Temp/360zip$Temp/360$0/
日志: 初始 PrivatePath = NULL
调用程序集: DependencyWalker, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null。
===
日志: 此绑定从 default 加载上下文开始。
日志: 未找到应用程序配置文件。
日志: 使用主机配置文件:
日志: 使用 C:\Windows\Microsoft.NET\Framework\v4.0.30319\config\machine.config 的计算机配置文件。
日志: 策略后引用: System.Threading.Tasks.Extensions, Version=4.2.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
日志: 相同的绑定已出现过,因 hr = 0x80070002 而失败。
Dependency Walker
说到dll的引用,就有必要提一提非常有名的一个工具Dependency Walker,它可以查看PE文件的引用情况,但是这个程序很久没有更新了,对.NET很不友好。我找了一下,发现几个替代:
- Dependencies
这个工具可以认为是Dependency Walker的升级版,可以查看PE文件的引用信息,对.NET也可以支持,不过看的信息太少了,也无法显示缺失的情况。

- DependencyWalker.Net
这个工具是专门为.NET设计的,可以查看程序集的引用情况,我删除了我插件的几个dll,然后看就是这个样子,一目了然,版本号也非常清楚。

通过这些工具,可以帮我们找一找到底是dll的问题,也许会对“试图加载格式不正确的程序。”、“引用不匹配。”等引用相关问题有所帮助。
这些工具处理的大多是静态引用,对于使用动态引用的,一般是不支持的。
总结
插件式编程极大增强了程序的拓展能力,不过,在处理程序集引用的时候,需要非常小心不同插件带来的引用问题。程序设计的时候,可以使用不同文件夹隔离不同的插件dll,并通过一些技巧来加载(可以查看之前的文章)和隔离,这样,就可以避免出现不匹配的错误了。
参考资料
- https://stackoverflow.com/questions/3267009/get-file-version-and-assembly-version-of-dll-files-in-the-current-directory-and
How to resolve .NET reference and NuGet package version conflicts
General | .NET , csharp , NuGet / Apr 24, 2018 
Some problems in programming seem to stay and bother us forever. Much like cockroaches, these problems resist technological advancements and increasing human knowledge. One such problem is the infamous DLL Hell (and variations of it).
The original DLL hell issue was this: Several applications use a shared DLL file. Then, one of the applications updated the DLL file and now the other applications no longer work. In .NET, this issue is solved. We will usually ship DLL files separately for each application. Alternatively, we can use the GAC for shared DLL’s, which supports versioning. This means we can store different versions of the same DLL and the different applications will load their intended version.
In the modern world, we are dependent on dozens of libraries. These in turn, depend on dozens more and we end up with hundreds or thousands of dependencies. So now the problem arises for a single application. What to do when several of its projects depend on different version of the same assembly?
Here’s an example: project A might use log4net V1.1 and project B uses log4net V1.2. Both DLL files are copied to the output folder, but there can be only one log4net.dll file. As a result, our application will fail at runtime when trying to load the version that wasn’t copied.
Here’s another scenario: We reference project A with NuGet which references System.Net.Http v4.5. We also reference project B with NuGet which references System.Net.Http v4.0. This phenomenon is knows as NuGet Hell. The result is the same and our application will fail with:

Could not load file or assembly or one of its dependencies. The located assemly’s manifest definition does not match the assembly reference.
If you ran into this exception, you are in the right place.
Luckily, Microsoft put a lot of thought into this issue and there are a lot of things we can do about it.
If possible, resolve to a single reference version
The best solution is to remove the need for different reference versions. Sometimes it’s as easy as going to NuGet manager and changing the versions. This is widely referred as DLL Hell and isn’t in the scope of this article. Some resources to help with DLL Hell are: [1] , [2] , [3]
Sometimes tough, you can’t depend on a single reference version due to all kind of reasons. The references might be referenced by a 3rd party that you can’t edit or you might have limitations like .NET framework target. If you’re in one of those times, this article is for you.
Use a single reference versions or load versions side-by-side
First of all, there’s an important decision to make: Do we want to force our projects to use the same reference version? Or do we want to load different versions side by side and use both?
The CLR does supports side-by-side loading. which means multiple versions of the same assembly are loaded and act as different modules. This can be problematic. Each assembly version doesn’t expect there’s another instance of it loaded. The different versions might fight for resources or get in each other’s way somehow .
Having said that, you can’t always use a single version, since the referencing projects might rely on features that exist only in their respective referenced versions.
In solution #1, we will how to force a specific version with Binding Redirects. Solution #2 through #4 will show various methods to achieve side-by-side loading.
If possible, prefer Binding Redirect to side-by-side loading
Solution 1: Use a single assembly version with Binding Redirect
In our log4net example, project A uses log4net 1.2.11 and project B uses log4net 1.2.15. We’ve noticed from the exception screenshot above that log4net v1.2.15 fails to load. When the debugger breaks on the Exception, we can open the Modules window (Debug -> Windows -> Modules) and see which version is actually loaded (If any)

The Modules shows that log4net 1.02.11 is loaded.
We can then force our project to use the loaded assembly with binding redirect . To make this work, we’ll need to add the following code to App.config of the assembly referencing log4net 1.02.15. If App.config doesn’t exist, create it.
<pre class="lang:default mark:6-13 decode:true"><?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
</startup>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="log4net" publicKeyToken="669e0ddf0bb1aa2a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-5.0.0.0" newVersion="1.2.11" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
Note that the runtime section is the one to be added.
We basically tell the runtime that whenever log4net in versions 0.0 – 5.0 is required, use version 1.2.11 instead.
Strong names and the GAC
As mentioned, the CLR program has the ability to load different versions of the same assembly. It’s easier to do that by signing the assembly with a strong name. This creates a unique identity to the assembly and allows to use some elegant solutions to work with different versions.
When using strong-named assemblies, they can be registered to the Global Assembly Cache (GAC). This is a sharing mechanism for common assemblies. It’s also very convenient for storing and loading different versions.
Strong name signing has some disadvantages as well.
We’ll see how to use the GAC to load different assemblies side-by-side later on in Solution 4. If you can’t or don’t want to sign the references with a strong name you can use AssemblyResolve.
Solution 2: Override AssemblyResolve for side-by-side loading (No need for strong names)
Let’s say we have 2 assemblies called Lib1 with different versions 1.0 and 1.1, which are referenced by different projects.
Let’s set CopyLocal to False for both references. We’ll need to copy the assemblies to different subfolders in the output folder, say to V10\Lib1.dll and V11\Lib1.dll. It’s important to have the DLL files in the output folder since this is the folder that will be deployed.
Copying the .dll files can be done with a post-build event (see explanation and some examples ). The output folder should look like this:

Since the assemblies aren’t in the output folder (they are in sub-folders), the assemblies will fail to load. Let’s register to AssemblyResolve event, which fires when an assembly’s failed to load, like this:
static void Main(string[] args)
{
AppDomain.CurrentDomain.AssemblyResolve += (sender, resolveArgs) =>
{
string assemblyInfo = resolveArgs.Name;// e.g "Lib1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
var parts = assemblyInfo.Split(',');
string name = parts[0];
var version = Version.Parse(parts[1].Split('=')[1]);
string fullName;
if (name == "Lib1" && version.Major == 1 && version.Minor == 0)
{
fullName = new FileInfo(@"V10\Lib1.dll").FullName;
}
else if (name == "Lib1" && version.Major == 1 && version.Minor == 1)
{
fullName = new FileInfo(@"V11\Lib1.dll").FullName;
}
else
{
return null;
}
return Assembly.LoadFile(fullName);
};
On each failed assembly load, we will parse the required version and manually load the assembly from the subfolders.
This is the only solution I was able to implement to load multiple version of an unsigned assembly. But, if we are working with strongly named assemblies, there are more elegant solutions.
Solution 3: Copy assemblies to different folders and use <codebase> to load them side-by-side (Requires strong names)
Let’s take a similar scenario. Suppose we have 2 assemblies called StrongNameLib with versions 1.0 and 1.1, which are referenced by different projects.
We’ll need to set CopyLocal to False for both of them and copy them to subfolders in the output folder with a post-build event. So far, exactly like in the previous solution.
We’ll need to find out the public key token, which is easy enough . Now, we can edit App.config (or create it, if it doesn’t exist) and add <codebase> nodes in the following way:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="StrongNameLib" culture="neutral" publicKeyToken="6a770f8bdf3d476a" />
<codeBase version="1.0.0.0" href="StrongNameLibV10/StrongNameLib.dll"/>
<codeBase version="1.1.0.0" href="StrongNameLibV11/StrongNameLib.dll"/>
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
We’re basically telling the CLR where to go to resolve each version.
Resolving with <codebase> like this only works with strongly typed assemblies. When the assembly isn’t strongly typed, the version property is ignored and the first <codebase> node is taken.
Solution 4: Install assemblies to the Global Assembly Cache (GAC)
This is the recommended solution for common libraries since the GAC will share libraries across applications. You can a install both assemblies to the GAC with gacutil.exe. It’s a matter of a single command prompt execution. For example:
<pre class="theme:cisco-router lang:default decode:true">gacutil.exe -i "c:\Dev\Project\Debug\MyLib.dll"
Read more on using gacutil in this tutorial . Note that there’s a different gacutil for each .NET version.
Once installed, set the references` CopyLocal property to False, since there’s no need for them to be in the output folder. This is it, we’re done. According to the runtime lookup order , the CLR will look in the GAC before checking the output folder. Since both assemblies are installed in the GAC, the runtime will find and load the correct versions.
The only caveat here is that unlike in the other solutions, where the output folder could be deployed as is, you’ll have to take care to install the assemblies in the GAC of the deployed computer. The installation could be included in the installer, installed manually on the cloud or added to the continuous deployment process.
Solution 5: Repack your libraries into a new assembly
Let’s assume you have the following conflict situation:

If you want to load both version of Newtonsoft.Json side by side, you can use ILMerge or il-repack to pack one of the branches into a new assembly (I recommend il-repack out of the two). Those solutions will take several input assembly and merge them into a new assembly. In this case, you can take Library A + Newtonsoft.Json 9 and merge into a new assembly called XYZ.dll. Once done, you can reference XYZ.dll and Newtonsoft.Json 8 without any conflict. Alternatively, you can pack Newtonsoft.Json 8 into a new assembly QWE.dll and then reference Newtonsoft.Json 9 and QWE.dll without any conflicts. This solution doesn’t require strongly-named assemblies.
Summary and resources
We saw the new version of DLL Hell and a lot of ways to deal with it. As mentioned, these solutions should be used with caution.
Here are more resources on the subject:
- Understanding How Assemblies Load in C# .NET
- Strong-Named Assemblies
- How NuGet resolves package dependencies
- Specifying an Assembly’s location with <codebase> and <probing>
- NuGet versioning Part 1: taking on DLL Hell