以下实验代码/项目在VS2017中使用netcore 2.0和netstandard 2.0。假设我有两个版本的第三方dll v1.0.0.0和v2.0.0.0,它们仅包含一个类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:没有项目引用的类库
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:具有项目参考Wrapper.Abstractions和dll参考ThirdPartyDependency v1.0.0.0的类库
namespace Wrapper.Implementation
{
public class ValueLoader : IValueLoader
{
public string GetValue()
{
return Constants.TestValue;
}
}
}
Wrapper.V2:具有项目参考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();
}
}
STEPS
在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 \
用在步骤3中复制的正确的绝对v2路径替换AssemblyLoadTest.Program.Main中的FULL-PATH-TO
运行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:'无法加载文件或程序集 'ThirdPartyDependency,版本= 2.0.0.0,文化=中性, PublicKeyToken = null”。无法找到或加载特定文件。 (来自HRESULT的异常:0x80131621)'
问题1:为什么带有绝对路径的LoadFile可以按预期工作,而带有相对路径的LoadFrom不起作用,而带有相对路径的LoadFrom在第一个if
语句中适用于v1.0.0.0?
规则1:检查是否已注册AppDomain.AssemblyResolve(最高优先级)
规则2:否则,请检查程序集是否已加载。
规则3:否则,请在文件夹中搜索程序集(可以在probing和codeBase中进行配置。
在Test3中未注册AssemblyResolve的地方,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\2.0.0.0\ThirdPartyDependency.dll</HintPath>
</Reference>
答案 0 :(得分:0)
Vitek Karas的好答案,原始链接为here。
不幸的是,您描述的所有行为目前都是按设计的。这并不意味着它是直观的(完全不是)。让我尝试解释一下。
程序集绑定基于AssemblyLoadContext(ALC)进行。每个ALC只能加载任何给定程序集的一个版本(因此,只有一个给定简单名称的程序集,而忽略版本,区域性,键等)。您可以创建一个新的ALC,然后可以再次加载具有相同或不同版本的任何程序集。因此,ALC提供了绑定隔离。
您的.exe和相关程序集被加载到默认ALC中-默认ALC是在运行时开始时创建的。
Assembly.LoadFrom将尝试始终将指定文件加载到默认ALC中。让我在这里强调“尝试”一词。如果默认ALC已加载的程序集具有相同的名称,并且已加载的程序集等于或更高版本,则LoadFrom将成功,但是它将使用已加载的程序集(有效地忽略您指定的路径)。另一方面,如果已经加载的程序集的版本比您要加载的程序集的版本低,这将失败(我们无法第二次将同一程序集加载到同一ALC中)。
Assembly.LoadFile将指定的文件加载到新的ALC中-始终创建一个新的ALC。因此,负载将始终有效地成功(由于它在自己的ALC中,因此它不可能与任何东西发生冲突)。
现在就您的情况来看:
测试1 之所以行之有效,是因为您的ResolveAssembly事件处理程序将两个程序集加载到单独的ALC中(LoadFile将创建一个新的程序集,因此第一个程序集将使用默认的ALC,而第二个程序集将使用其自己的)。
Test2 这将失败,因为LoadFrom尝试将程序集加载到默认ALC中。当它调用第二个LoadFrom时,该故障实际上发生在AssemblyResolve处理程序中。第一次将v1加载到Default中,第二次尝试将v2加载到Default中-失败,因为Default已经加载了v1。
Test3 这以相同的方式失败,因为它在内部基本上完全执行Test2的操作。 Assembly.LoadFrom还为AssemblyResolve注册事件处理程序,并确保可以从同一文件夹中加载相关程序集。因此,在您的情况下,v1 \ Wrapper.V1.dll会将其依赖项解析为v1 \ ThirdPartyDependency.dll,因为它在磁盘上紧挨着它。然后,对于v2,它将尝试执行相同的操作,但是v1已经加载,因此它就像在Test2中一样失败。请记住,LoadFrom将所有内容加载到默认ALC中,因此可能发生冲突。
您的问题:
问题1 LoadFile之所以有效,是因为它将程序集加载到其自己的ALC中,该ALC提供了完全的隔离,因此永远不会发生任何冲突。 LoadFrom将程序集加载到默认ALC中,因此,如果该程序集已经加载了具有相同名称的程序集,则可能会发生冲突。
问题2 实际上不会忽略该版本。该版本很受好评,这就是为什么Test2和Test3失败的原因。但是我可能无法正确理解这个问题-我不清楚您要问的是哪种情况。
CLR绑定顺序 您描述的规则顺序是不同的。 基本上是:
规则3实际上不存在。 .NET Core没有探测路径或代码库的概念。对于由应用程序静态引用的程序集,它确实可以执行此操作,但是对于动态加载的程序集,则不会执行任何探测(如上所述,LoadFrom从与父级相同的文件夹中加载依赖程序集除外)。
解决方案 要使其完全起作用,您需要执行以下任一操作:
将LoadFile与AssemblyResolve处理程序一起使用。但是这里的问题是,如果您将本身具有其他依赖关系的程序集加载到File中,则也将需要处理您的处理程序中的那些程序(您会失去从同一文件夹加载依赖项的LoadFrom的“好”行为)
实施自己的ALC,该ALC处理所有依赖项。从技术上讲,这是更清洁的解决方案,但可能还会做更多工作。就这一点而言,这相似,如果需要,您仍然必须从同一文件夹中执行加载。
我们正在积极努力简化这种情况。今天,它们是可行的,但是很难。该计划是为了解决.NET Core 3的问题。我们也很清楚这方面缺乏文档/指南。最后但并非最不重要的一点是,我们正在努力改进错误消息,这些消息目前非常混乱。