【问题标题】:Strange behavior when loading assemblies and its dependencies programatically以编程方式加载程序集及其依赖项时的奇怪行为
【发布时间】:2018-08-08 04:28:49
【问题描述】:

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

步骤

  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. 将 AssemblyLoadTest.Program.Main 中的 FULL-PATH-TO 替换为您在步骤 3 中复制的正确 v2 绝对路径

  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() 处引发异常

System.IO.FileLoadException: '无法加载文件或程序集 '第三方依赖,版本=2.0.0.0,文化=中性, PublicKeyToken=null'。无法找到或加载特定文件。 (HRESULT 异常:0x80131621)'

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

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

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

Rule2:否则检查程序集是否已加载。

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

在未注册 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\2.0.0.0\ThirdPartyDependency.dll</HintPath>
</Reference> 

【问题讨论】:

  • 发布到 github coreclr 团队问题,link

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


【解决方案1】:

来自Vitek Karas 的精彩回答,原始链接here

不幸的是,您描述的所有行为目前都是按设计的。这并不意味着它是直观的(它完全不是)。让我试着解释一下。

程序集绑定基于 AssemblyLoadContext (ALC) 发生。每个 ALC 只能加载任何给定程序集的一个版本(因此只有一个给定简单名称的程序集,忽略版本、文化、键等)。您可以创建一个新的 ALC,然后可以再次加载任何具有相同或不同版本的程序集。所以 ALC 提供了绑定隔离。

您的 .exe 和相关程序集被加载到默认 ALC - 它是在运行时开始时创建的。

Assembly.LoadFrom 将尝试将指定文件加载到默认 ALC - 总是。让我在这里强调“尝试”这个词。如果 Default ALC 已经加载了同名的程序集,并且已经加载的程序集是相同或更高的版本,则 LoadFrom 将成功,但它将使用已加载的程序集(有效地忽略您指定的路径)。另一方面,如果已加载的程序集的版本较低,那么您尝试加载的版本将失败(我们无法将相同的程序集第二次加载到同一个 ALC 中)。

Assembly.LoadFile 会将指定的文件加载到新的 ALC - 总是创建一个新的 ALC。所以加载实际上总是会成功(这不可能与任何东西发生冲突,因为它在它自己的 ALC 中)。

现在开始你的场景:

测试1 这是因为您的 ResolveAssembly 事件处理程序将两个程序集加载到单独的 ALC 中(LoadFile 将创建一个新程序集,因此第一个程序集进入默认 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 绑定顺序 您描述的规则顺序不同。 基本上是:

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

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

解决方案 要使其充分发挥作用,您需要执行以下任一操作:

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

  • 实现您自己的 ALC 来处理所有依赖项。这在技术上是更清洁的解决方案,但可能需要更多的工作。在这方面类似的是,如果需要,您仍然必须从同一文件夹实现加载。

我们正在积极努力使这样的场景变得简单。今天它们是可行的,但相当困难。计划是为 .NET Core 3 解决这个问题。我们也非常清楚该领域缺乏文档/指导。最后但同样重要的是,我们正在努力改进目前非常混乱的错误消息。

【讨论】:

  • 将文本用引号括起来,因为它是别人写的。
猜你喜欢
  • 2010-09-06
  • 1970-01-01
  • 1970-01-01
  • 2023-03-26
  • 1970-01-01
  • 1970-01-01
  • 2021-12-01
  • 2012-05-17
  • 1970-01-01
相关资源
最近更新 更多