以编程方式加载程序集及其依赖项时的奇怪行为
Strange behavior when loading assemblies and its dependencies programatically
以下实验codes/projects在VS2017中使用netcore 2.0和netstandard 2.0。假设我有两个版本的第三方 dll v1.0.0.0 和 v2.0.0.0,其中只包含一个 class Constants.cs
.
//ThirdPartyDependency.dll v1.0.0.0
public class Constants
{
public static readonly string TestValue = "test value v1.0.0.0";
}
//ThirdPartyDependency.dll v2.0.0.0
public class Constants
{
public static readonly string TestValue = "test value v2.0.0.0";
}
然后我创建了自己的名为 AssemblyLoadTest 的解决方案,其中包含:
Wrapper.Abstraction: class 没有项目引用的库
namespace Wrapper.Abstraction
{
public interface IValueLoader
{
string GetValue();
}
public class ValueLoaderFactory
{
public static IValueLoader Create(string wrapperAssemblyPath)
{
var assembly = Assembly.LoadFrom(wrapperAssemblyPath);
return (IValueLoader)assembly.CreateInstance("Wrapper.Implementation.ValueLoader");
}
}
}
Wrapper.V1: class 带有项目引用的库 Wrapper.Abstractions 和 dll 引用 ThirdPartyDependency v1.0.0.0
namespace Wrapper.Implementation
{
public class ValueLoader : IValueLoader
{
public string GetValue()
{
return Constants.TestValue;
}
}
}
Wrapper.V2: class 带有项目引用的库 Wrapper.Abstractions 和 dll 引用 ThirdPartyDependency v2.0.0.0
namespace Wrapper.Implementation
{
public class ValueLoader : IValueLoader
{
public string GetValue()
{
return Constants.TestValue;
}
}
}
AssemblyLoadTest:带有项目引用的控制台应用程序 Wrapper.Abstraction
class Program
{
static void Main(string[] args)
{
AppDomain.CurrentDomain.AssemblyResolve += (s, e) =>
{
Console.WriteLine($"AssemblyResolve: {e.Name}");
if (e.Name.StartsWith("ThirdPartyDependency, Version=1.0.0.0"))
{
return Assembly.LoadFrom(@"v1\ThirdPartyDependency.dll");
}
else if (e.Name.StartsWith("ThirdPartyDependency, Version=2.0.0.0"))
{
//return Assembly.LoadFrom(@"v2\ThirdPartyDependency.dll");//FlagA
return Assembly.LoadFile(@"C:\FULL-PATH-TO\v2\ThirdPartyDependency.dll");//FlagB
}
throw new Exception();
};
var v1 = ValueLoaderFactory.Create(@"v1\Wrapper.V1.dll");
var v2 = ValueLoaderFactory.Create(@"v2\Wrapper.V2.dll");
Console.WriteLine(v1.GetValue());
Console.WriteLine(v2.GetValue());
Console.Read();
}
}
步骤
在 DEBUG 中构建 AssemblyLoadTest
在DEBUG中构建Wrapper.V1工程,将Wrapper.V1\bin\Debug\netstandard2.0\中的文件复制到AssemblyLoadTest\bin\Debug\netcoreapp2.0\v1\
在DEBUG中构建Wrapper.V2工程,将Wrapper.V2\bin\Debug\netstandard2.0\中的文件复制到AssemblyLoadTest\bin\Debug\netcoreapp2.0\v2\
将 AssemblyLoadTest.Program.Main 中的 FULL-PATH-TO 替换为您在步骤 3 中复制的正确的绝对 v2 路径
运行 AssemblyLoadTest - Test1
注释 FlagB 行并取消注释 FlagA 行,运行 AssemblyLoadTest - Test2
评论AppDomain.CurrentDomain.AssemblyResolve、运行 AssemblyLoadTest - Test3
我的结果和问题:
Test1 成功并按预期打印 v1.0.0.0 和 v2.0.0.0
Test2 在 v2.GetValue()
抛出异常
System.IO.FileLoadException: 'Could not load file or assembly
'ThirdPartyDependency, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=null'. Could not find or load a specific file.
(Exception from HRESULT: 0x80131621)'
问题1:为什么在第一个if
语句中,使用绝对路径的LoadFile可以正常工作,而使用相对路径的LoadFrom不工作,同时使用相对路径的LoadFrom适用于v1.0.0.0?
- Test3 在同一位置失败并出现上述相同的异常,这里我的理解是 CLR 定位具有以下优先级规则的程序集:
规则 1:检查 AppDomain.AssemblyResolve 是否已注册(最高优先级)
规则 2:否则检查程序集是否已加载。
规则 3:否则在文件夹中搜索程序集(可以在 probing and codeBase 中配置。
此处在未注册 AssemblyResolve 的 Test3 中,v1.GetValue
有效,因为 Rule1 和 Rule2 是 N/A,AssemblyLoadTest\bin\Debug\netcoreapp2.1\v1
在 Rule3 扫描候选中。执行v2.GetValue
时,Rule1还是N/A,但是这里应用了Rule2(如果应用了Rule3,为什么会出现异常?)
问题 2:为什么使用
甚至 Wrapper.V2 引用 ThirdPartyDependency.dll 时版本也被忽略
<Reference Include="ThirdPartyDependency, Version=2.0.0.0">
<HintPath>..\lib\ThirdPartyDependency.0.0.0\ThirdPartyDependency.dll</HintPath>
</Reference>
Vitek Karas, original link here 的出色回答。
不幸的是,您描述的所有行为目前都是设计好的。这并不意味着它是直观的(它完全不是)。让我试着解释一下。
程序集绑定基于 AssemblyLoadContext (ALC)。每个 ALC 只能加载任何给定程序集的一个版本(因此只有一个给定简单名称的程序集,忽略版本、区域性、键等)。您可以创建一个新的 ALC,然后可以使用相同或不同的版本再次加载任何程序集。所以 ALC 提供绑定隔离。
您的 .exe 和相关程序集被加载到默认 ALC - 一个在运行时开始时创建的。
Assembly.LoadFrom 将始终尝试将指定文件加载到默认 ALC。让我在这里强调 "try" 这个词。如果默认 ALC 已加载同名程序集,并且已加载程序集的版本相同或更高,则 LoadFrom 将成功,但它将使用已加载的程序集(有效地忽略您指定的路径)。另一方面,如果已经加载的程序集的版本低于您尝试加载的版本 - 这将失败(我们无法将同一程序集第二次加载到同一 ALC 中)。
Assembly.LoadFile 将指定的文件加载到新的 ALC - 总是创建一个新的 ALC。所以加载总是会有效地成功(因为它在它自己的 ALC 中,所以它不可能与任何东西发生碰撞)。
现在开始你的场景:
测试 1
这是有效的,因为您的 ResolveAssembly 事件处理程序将两个程序集加载到单独的 ALC 中(LoadFile 将创建一个新的 ALC,因此第一个程序集进入默认的 ALC,第二个进入它自己的)。
测试2
这失败了,因为 LoadFrom 试图将程序集加载到默认 ALC 中。失败实际上发生在 AssemblyResolve 处理程序调用第二个 LoadFrom 时。第一次将 v1 加载到 Default 中,第二次尝试将 v2 加载到 Default 中 - 但失败了,因为 Default 已经加载了 v1。
测试 3
这以同样的方式失败,因为它在内部基本上完全与 Test2 所做的一样。 Assembly.LoadFrom 还为 AssemblyResolve 注册事件处理程序,这确保可以从同一文件夹加载依赖程序集。所以在你的情况下 v1\Wrapper.V1.dll 将解析它对 v1\ThirdPartyDependency.dll 的依赖,因为它在磁盘上紧挨着它。然后对于 v2,它会尝试做同样的事情,但是 v1 已经加载,所以它失败了,就像在 Test2 中一样。请记住,LoadFrom 将所有内容加载到默认 ALC 中,因此可能会发生冲突。
您的问题:
问题1
LoadFile 之所以有效,是因为它将程序集加载到它自己的 ALC 中,这提供了完全隔离,因此永远不会有任何冲突。 LoadFrom 将程序集加载到默认 ALC 中,因此如果已经加载了同名程序集,则可能会发生冲突。
问题2
版本实际上没有被忽略。该版本受到尊重,这就是 Test2 和 Test3 失败的原因。但我可能没有正确理解这个问题 - 我不清楚你是在什么情况下问的。
CLR 绑定顺序
您描述的规则顺序不同。
基本上是:
- 规则 2 - 如果已经加载 - 使用它(如果已经加载更高版本,则使用它)
- 规则 1 - 如果一切都失败了 - 作为最后的手段 - 调用 AppDomain.AssemblyResolve
规则 3 实际上并不存在。 .NET Core 没有探测路径或代码库的概念。它对应用程序静态引用的程序集有点作用,但对于动态加载的程序集,不执行探测(除了 LoadFrom 从与父级相同的文件夹加载依赖程序集,如上所述)。
解决方案
要使其充分发挥作用,您需要执行以下任一操作:
将 LoadFile 与您的 AssemblyResolve 处理程序一起使用。但这里的问题是,如果您 LoadFile 一个本身具有其他依赖项的程序集,您还需要在处理程序中处理这些依赖项(您失去了 LoadFrom 的 "nice" 行为,它从同一文件夹加载依赖项)
实施您自己的 ALC 来处理所有依赖项。这在技术上是更清洁的解决方案,但可能需要更多工作。在这方面类似,如果需要,您仍然必须从同一文件夹实现加载。
我们正在积极努力让这样的场景变得简单。今天它们是可行的,但相当困难。计划是为 .NET Core 3 解决这个问题。我们也非常清楚这方面缺少 documentation/guidance。最后但同样重要的是,我们正在努力改进目前非常混乱的错误消息。
以下实验codes/projects在VS2017中使用netcore 2.0和netstandard 2.0。假设我有两个版本的第三方 dll v1.0.0.0 和 v2.0.0.0,其中只包含一个 class Constants.cs
.
//ThirdPartyDependency.dll v1.0.0.0
public class Constants
{
public static readonly string TestValue = "test value v1.0.0.0";
}
//ThirdPartyDependency.dll v2.0.0.0
public class Constants
{
public static readonly string TestValue = "test value v2.0.0.0";
}
然后我创建了自己的名为 AssemblyLoadTest 的解决方案,其中包含:
Wrapper.Abstraction: class 没有项目引用的库
namespace Wrapper.Abstraction
{
public interface IValueLoader
{
string GetValue();
}
public class ValueLoaderFactory
{
public static IValueLoader Create(string wrapperAssemblyPath)
{
var assembly = Assembly.LoadFrom(wrapperAssemblyPath);
return (IValueLoader)assembly.CreateInstance("Wrapper.Implementation.ValueLoader");
}
}
}
Wrapper.V1: class 带有项目引用的库 Wrapper.Abstractions 和 dll 引用 ThirdPartyDependency v1.0.0.0
namespace Wrapper.Implementation
{
public class ValueLoader : IValueLoader
{
public string GetValue()
{
return Constants.TestValue;
}
}
}
Wrapper.V2: class 带有项目引用的库 Wrapper.Abstractions 和 dll 引用 ThirdPartyDependency v2.0.0.0
namespace Wrapper.Implementation
{
public class ValueLoader : IValueLoader
{
public string GetValue()
{
return Constants.TestValue;
}
}
}
AssemblyLoadTest:带有项目引用的控制台应用程序 Wrapper.Abstraction
class Program
{
static void Main(string[] args)
{
AppDomain.CurrentDomain.AssemblyResolve += (s, e) =>
{
Console.WriteLine($"AssemblyResolve: {e.Name}");
if (e.Name.StartsWith("ThirdPartyDependency, Version=1.0.0.0"))
{
return Assembly.LoadFrom(@"v1\ThirdPartyDependency.dll");
}
else if (e.Name.StartsWith("ThirdPartyDependency, Version=2.0.0.0"))
{
//return Assembly.LoadFrom(@"v2\ThirdPartyDependency.dll");//FlagA
return Assembly.LoadFile(@"C:\FULL-PATH-TO\v2\ThirdPartyDependency.dll");//FlagB
}
throw new Exception();
};
var v1 = ValueLoaderFactory.Create(@"v1\Wrapper.V1.dll");
var v2 = ValueLoaderFactory.Create(@"v2\Wrapper.V2.dll");
Console.WriteLine(v1.GetValue());
Console.WriteLine(v2.GetValue());
Console.Read();
}
}
步骤
在 DEBUG 中构建 AssemblyLoadTest
在DEBUG中构建Wrapper.V1工程,将Wrapper.V1\bin\Debug\netstandard2.0\中的文件复制到AssemblyLoadTest\bin\Debug\netcoreapp2.0\v1\
在DEBUG中构建Wrapper.V2工程,将Wrapper.V2\bin\Debug\netstandard2.0\中的文件复制到AssemblyLoadTest\bin\Debug\netcoreapp2.0\v2\
将 AssemblyLoadTest.Program.Main 中的 FULL-PATH-TO 替换为您在步骤 3 中复制的正确的绝对 v2 路径
运行 AssemblyLoadTest - Test1
注释 FlagB 行并取消注释 FlagA 行,运行 AssemblyLoadTest - Test2
评论AppDomain.CurrentDomain.AssemblyResolve、运行 AssemblyLoadTest - Test3
我的结果和问题:
Test1 成功并按预期打印 v1.0.0.0 和 v2.0.0.0
Test2 在
v2.GetValue()
抛出异常
System.IO.FileLoadException: 'Could not load file or assembly 'ThirdPartyDependency, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null'. Could not find or load a specific file. (Exception from HRESULT: 0x80131621)'
问题1:为什么在第一个if
语句中,使用绝对路径的LoadFile可以正常工作,而使用相对路径的LoadFrom不工作,同时使用相对路径的LoadFrom适用于v1.0.0.0?
- Test3 在同一位置失败并出现上述相同的异常,这里我的理解是 CLR 定位具有以下优先级规则的程序集:
规则 1:检查 AppDomain.AssemblyResolve 是否已注册(最高优先级)
规则 2:否则检查程序集是否已加载。
规则 3:否则在文件夹中搜索程序集(可以在 probing and codeBase 中配置。
此处在未注册 AssemblyResolve 的 Test3 中,v1.GetValue
有效,因为 Rule1 和 Rule2 是 N/A,AssemblyLoadTest\bin\Debug\netcoreapp2.1\v1
在 Rule3 扫描候选中。执行v2.GetValue
时,Rule1还是N/A,但是这里应用了Rule2(如果应用了Rule3,为什么会出现异常?)
问题 2:为什么使用
甚至 Wrapper.V2 引用 ThirdPartyDependency.dll 时版本也被忽略<Reference Include="ThirdPartyDependency, Version=2.0.0.0">
<HintPath>..\lib\ThirdPartyDependency.0.0.0\ThirdPartyDependency.dll</HintPath>
</Reference>
Vitek Karas, original link here 的出色回答。
不幸的是,您描述的所有行为目前都是设计好的。这并不意味着它是直观的(它完全不是)。让我试着解释一下。
程序集绑定基于 AssemblyLoadContext (ALC)。每个 ALC 只能加载任何给定程序集的一个版本(因此只有一个给定简单名称的程序集,忽略版本、区域性、键等)。您可以创建一个新的 ALC,然后可以使用相同或不同的版本再次加载任何程序集。所以 ALC 提供绑定隔离。
您的 .exe 和相关程序集被加载到默认 ALC - 一个在运行时开始时创建的。
Assembly.LoadFrom 将始终尝试将指定文件加载到默认 ALC。让我在这里强调 "try" 这个词。如果默认 ALC 已加载同名程序集,并且已加载程序集的版本相同或更高,则 LoadFrom 将成功,但它将使用已加载的程序集(有效地忽略您指定的路径)。另一方面,如果已经加载的程序集的版本低于您尝试加载的版本 - 这将失败(我们无法将同一程序集第二次加载到同一 ALC 中)。
Assembly.LoadFile 将指定的文件加载到新的 ALC - 总是创建一个新的 ALC。所以加载总是会有效地成功(因为它在它自己的 ALC 中,所以它不可能与任何东西发生碰撞)。
现在开始你的场景:
测试 1 这是有效的,因为您的 ResolveAssembly 事件处理程序将两个程序集加载到单独的 ALC 中(LoadFile 将创建一个新的 ALC,因此第一个程序集进入默认的 ALC,第二个进入它自己的)。
测试2 这失败了,因为 LoadFrom 试图将程序集加载到默认 ALC 中。失败实际上发生在 AssemblyResolve 处理程序调用第二个 LoadFrom 时。第一次将 v1 加载到 Default 中,第二次尝试将 v2 加载到 Default 中 - 但失败了,因为 Default 已经加载了 v1。
测试 3 这以同样的方式失败,因为它在内部基本上完全与 Test2 所做的一样。 Assembly.LoadFrom 还为 AssemblyResolve 注册事件处理程序,这确保可以从同一文件夹加载依赖程序集。所以在你的情况下 v1\Wrapper.V1.dll 将解析它对 v1\ThirdPartyDependency.dll 的依赖,因为它在磁盘上紧挨着它。然后对于 v2,它会尝试做同样的事情,但是 v1 已经加载,所以它失败了,就像在 Test2 中一样。请记住,LoadFrom 将所有内容加载到默认 ALC 中,因此可能会发生冲突。
您的问题:
问题1 LoadFile 之所以有效,是因为它将程序集加载到它自己的 ALC 中,这提供了完全隔离,因此永远不会有任何冲突。 LoadFrom 将程序集加载到默认 ALC 中,因此如果已经加载了同名程序集,则可能会发生冲突。
问题2 版本实际上没有被忽略。该版本受到尊重,这就是 Test2 和 Test3 失败的原因。但我可能没有正确理解这个问题 - 我不清楚你是在什么情况下问的。
CLR 绑定顺序 您描述的规则顺序不同。 基本上是:
- 规则 2 - 如果已经加载 - 使用它(如果已经加载更高版本,则使用它)
- 规则 1 - 如果一切都失败了 - 作为最后的手段 - 调用 AppDomain.AssemblyResolve
规则 3 实际上并不存在。 .NET Core 没有探测路径或代码库的概念。它对应用程序静态引用的程序集有点作用,但对于动态加载的程序集,不执行探测(除了 LoadFrom 从与父级相同的文件夹加载依赖程序集,如上所述)。
解决方案 要使其充分发挥作用,您需要执行以下任一操作:
将 LoadFile 与您的 AssemblyResolve 处理程序一起使用。但这里的问题是,如果您 LoadFile 一个本身具有其他依赖项的程序集,您还需要在处理程序中处理这些依赖项(您失去了 LoadFrom 的 "nice" 行为,它从同一文件夹加载依赖项)
实施您自己的 ALC 来处理所有依赖项。这在技术上是更清洁的解决方案,但可能需要更多工作。在这方面类似,如果需要,您仍然必须从同一文件夹实现加载。
我们正在积极努力让这样的场景变得简单。今天它们是可行的,但相当困难。计划是为 .NET Core 3 解决这个问题。我们也非常清楚这方面缺少 documentation/guidance。最后但同样重要的是,我们正在努力改进目前非常混乱的错误消息。