以编程方式加载程序集及其依赖项时的行为异常

时间:2018-08-08 04:28:50

标签: c# .net .net-core clr .net-standard

以下实验代码/项目在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

  1. 在DEBUG中构建AssemblyLoadTest

  2. 在DEBUG中构建Wrapper.V1项目,将Wrapper.V1 \ bin \ Debug \ netstandard2.0 \中的文件复制到AssemblyLoadTest \ bin \ Debug \ netcoreapp2.0 \ v1 \

  3. 在DEBUG中构建Wrapper.V2项目,将Wrapper.V2 \ bin \ Debug \ netstandard2.0 \中的文件复制到AssemblyLoadTest \ bin \ Debug \ netcoreapp2.0 \ v2 \

  4. 用在步骤3中复制的正确的绝对v2路径替换AssemblyLoadTest.Program.Main中的FULL-PATH-TO

  5. 运行AssemblyLoadTest-Test1

  6. 注释FlagB行和取消注释FlagA行,运行AssemblyLoadTest-Test2

  7. 评论AppDomain.CurrentDomain.AssemblyResolve,运行AssemblyLoadTest-Test3

我的结果和问题:

  1. Test1成功,并按预期显示v1.0.0.0和v2.0.0.0

  2. Test2在v2.GetValue()

  3. 处引发异常
  

System.IO.FileLoadException:'无法加载文件或程序集   'ThirdPartyDependency,版本= 2.0.0.0,文化=中性,   PublicKeyToken = null”。无法找到或加载特定文件。   (来自HRESULT的异常:0x80131621)'

问题1:为什么带有绝对路径的LoadFile可以按预期工作,而带有相对路径的LoadFrom不起作用,而带有相对路径的LoadFrom在第一个if语句中适用于v1.0.0.0?

  1. Test3失败,并且在同一位置出现上述相同的异常,在这里我的理解是CLR使用以下优先级规则来定位程序集:

规则1:检查是否已注册AppDomain.AssemblyResolve(最高优先级)

规则2:否则,请检查程序集是否已加载。

规则3:否则,请在文件夹中搜索程序集(可以在probingcodeBase中进行配置。

在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> 

1 个答案:

答案 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绑定顺序 您描述的规则顺序是不同的。 基本上是:

  • 规则2-如果已经加载-使用它(包括如果已经加载了更高版本,则使用它)
  • 规则1 –如果一切失败–作为最后的选择-调用AppDomain.AssemblyResolve

规则3实际上不存在。 .NET Core没有探测路径或代码库的概念。对于由应用程序静态引用的程序集,它确实可以执行此操作,但是对于动态加载的程序集,则不会执行任何探测(如上所述,LoadFrom从与父级相同的文件夹中加载依赖程序集除外)。

解决方案 要使其完全起作用,您需要执行以下任一操作:

  • 将LoadFile与AssemblyResolve处理程序一起使用。但是这里的问题是,如果您将本身具有其他依赖关系的程序集加载到File中,则也将需要处理您的处理程序中的那些程序(您会失去从同一文件夹加载依赖项的LoadFrom的“好”行为)

  • 实施自己的ALC,该ALC处理所有依赖项。从技术上讲,这是更清洁的解决方案,但可能还会做更多工作。就这一点而言,这相似,如果需要,您仍然必须从同一文件夹中执行加载。

我们正在积极努力简化这种情况。今天,它们是可行的,但是很难。该计划是为了解决.NET Core 3的问题。我们也很清楚这方面缺乏文档/指南。最后但并非最不重要的一点是,我们正在努力改进错误消息,这些消息目前非常混乱。