代码之家  ›  专栏  ›  技术社区  ›  Cheng Chen

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

  •  2
  • Cheng Chen  · 技术社区  · 7 年前

    以下实验代码/项目在VS2017中使用NetCore2.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的类库

    命名空间包装器.实现
    {
    公共类值加载器:ivalueLoader
    {
    公共字符串GetValue()
    {
    返回常量.testValue;
    }
    }
    }
    

    assemblyloadtest:带有项目引用包装器的控制台应用程序。抽象

    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. 调试中的生成程序集负载测试

    2. 在调试中生成wrapper.v1项目,将wrapper.v1\bin\debug\netstandard2.0中的文件复制到assemblyloadtest\bin\debug\netcoreapp2.0\v1\

    3. 在调试中生成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. 注释标记行并取消标记行的注释,运行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=空。无法找到或加载特定文件。 (来自hresult的异常:0x80131621)'

    问题1:为什么具有绝对路径的loadfile按预期工作,而具有相对路径的loadfrom不工作,同时第一个具有相对路径的loadfrom工作于v1.0.0.0 if 声明?

    1. test3在同一个地方失败,上面的异常相同,我的理解是clr使用以下优先级规则定位程序集:

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

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

    规则3:否则在文件夹中搜索程序集(可以在 probing codeBase

    在test3中,没有注册assemblyresolve, v1.GetValue 因为规则1和规则2不适用, AssemblyLoadTest\bin\Debug\netcoreapp2.1\v1 在规则3扫描候选项中。执行时 v2.GetValue ,规则1仍然是不适用的,但是这里应用了规则2(如果应用了规则3,为什么是例外?)

    问题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 回复  |  直到 7 年前
        1
  •  0
  •   Cheng Chen    7 年前

    很好的回答来自 Vitek Karas ,原始链接 here

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

    程序集绑定基于assemblyloadcontext(alc)进行。每个ALC只能加载任何给定程序集的一个版本(因此只有一个给定简单名称的程序集,忽略版本、区域性、键等)。您可以创建一个新的ALC,然后可以使用相同或不同的版本再次加载任何程序集。因此,ALC提供绑定隔离。

    您的.exe和相关程序集将加载到运行时开始时创建的默认alc-one中。

    assembly.loadfrom将尝试将指定的文件加载到默认的alc-always中。我来强调一下“试试”这个词。如果默认ALC已加载同名程序集,并且已加载的程序集的版本等于或更高,则LoadFrom将成功,但它将使用已加载的程序集(有效地忽略指定的路径)。另一方面,如果已经加载的程序集是较低版本,那么您尝试加载的程序集-这将失败(我们不能将同一程序集第二次加载到同一ALC中)。

    assembly.load file将把指定的文件加载到一个新的ALC中-总是创建一个新的ALC。因此,负载将始终有效地成功(这是不可能与任何东西发生碰撞的,因为它是在自己的ALC中)。

    所以现在来看看你的场景:

    测试1 这是因为resolveassembly事件处理程序将两个程序集加载到单独的ALC中(loadfile将创建一个新程序集,因此第一个程序集将转到默认ALC,第二个程序集将转到它自己的ALC中)。

    测试2 此操作失败,因为LoadFrom尝试将程序集加载到默认ALC中。当assemblyresolve处理程序调用第二个loadfrom时,该失败实际上发生在该处理程序中。第一次将v1加载到默认值时,第二次尝试将v2加载到默认值时失败,因为默认值已经加载了v1。

    测试3 这同样失败了,因为它在内部基本上与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核心没有探测路径或代码库的概念。对于应用程序静态引用的程序集,它有点类似,但对于动态加载的程序集,不执行探测(从与父程序相同的文件夹加载依赖程序集的loadfrom除外,如上所述)。

    解决 要使其完全工作,您需要执行以下任一操作:

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

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

    我们正在积极努力使这种情况变得容易。今天它们是可行的,但相当困难。计划是要有一些东西来解决.NET核心3的问题。我们也很清楚这方面缺乏文件/指导。最后但并非最不重要的是,我们正在努力改进错误消息,这目前非常令人困惑。