简体   繁体   English

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

[英]Strange behavior when loading assemblies and its dependencies programatically

The following experimental codes/projects are using netcore 2.0 and netstandard 2.0 in VS2017. 以下实验代码/项目在VS2017中使用netcore 2.0和netstandard 2.0。 Let's say I have two versions of a third party dll v1.0.0.0 and v2.0.0.0, which contains only one class Constants.cs . 假设我有第三方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";
}

Then I created my own solution named AssemblyLoadTest, which contains: 然后,我创建了自己的名为AssemblyLoadTest的解决方案,其中包含:

Wrapper.Abstraction: class library with no project references 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: class library with project reference Wrapper.Abstractions and dll reference ThirdPartyDependency v1.0.0.0 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: class library with project reference Wrapper.Abstractions and dll reference ThirdPartyDependency v2.0.0.0 Wrapper.V2:具有项目参考Wrapper.Abstractions和dll参考ThirdPartyDependency v2.0.0.0的类库

namespace Wrapper.Implementation
{
    public class ValueLoader : IValueLoader
    {
        public string GetValue()
        {
            return Constants.TestValue;
        }
    }
}

AssemblyLoadTest: console application with project reference Wrapper.Abstraction 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. Build AssemblyLoadTest in DEBUG 在DEBUG中建立AssemblyLoadTest

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

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

  4. Replace FULL-PATH-TO in AssemblyLoadTest.Program.Main with the correct absolute v2 path that you copied in step 3 用在步骤3中复制的正确的绝对v2路径替换AssemblyLoadTest.Program.Main中的FULL-PATH-TO

  5. Run AssemblyLoadTest - Test1 运行AssemblyLoadTest-Test1

  6. Comment FlagB line and uncomment FlagA line, run AssemblyLoadTest - Test2 注释FlagB行并取消注释FlagA行,运行AssemblyLoadTest-Test2

  7. Comment AppDomain.CurrentDomain.AssemblyResolve, run AssemblyLoadTest - Test3 注释AppDomain.CurrentDomain.AssemblyResolve,运行AssemblyLoadTest-Test3

My results and questions: 我的结果和问题:

  1. Test1 succeeds and prints v1.0.0.0 and v2.0.0.0 as expected Test1成功,并按预期打印v1.0.0.0和v2.0.0.0

  2. Test2 throws exception at v2.GetValue() Test2在v2.GetValue()引发异常

System.IO.FileLoadException: 'Could not load file or assembly 'ThirdPartyDependency, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null'. System.IO.FileLoadException:'无法加载文件或程序集'ThirdPartyDependency,Version = 2.0.0.0,Culture = neutral,PublicKeyToken = null'。 Could not find or load a specific file. 无法找到或加载特定文件。 (Exception from HRESULT: 0x80131621)' (来自HRESULT的异常:0x80131621)

Question1: Why LoadFile with absolute path works as expected, while LoadFrom with relative path not working, meanwhile LoadFrom with relative path works for v1.0.0.0 in the first if statement? 问题1:为什么具有绝对路径的LoadFile可以按预期工作,而具有相对路径的LoadFrom无法正常工作,而具有相对路径的LoadFrom在第一个if语句中适用于v1.0.0.0?

  1. Test3 fails with the same exception above at the same place, here my understanding is CLR locates the assemblies with the following priority rule: Test3在同一位置出现以上相同异常而失败,在这里我的理解是CLR使用以下优先级规则来定位程序集:

Rule1: Check if AppDomain.AssemblyResolve is registered (highest priority) 规则1:检查是否已注册AppDomain.AssemblyResolve(最高优先级)

Rule2: Otherwise check if the assembly is loaded. Rule2:否则,请检查组件是否已加载。

Rule3: Otherwise search the assembly in folders(can be configured in probing and codeBase . Rule3:否则,在文件夹中搜索程序集(可以在probingcodeBase中进行配置。

Here in Test3 where AssemblyResolve is not registered, v1.GetValue works because Rule1 and Rule2 is N/A, AssemblyLoadTest\\bin\\Debug\\netcoreapp2.1\\v1 is in Rule3 scan candidates. 在未注册AssemblyResolve的Test3中, v1.GetValue起作用是因为Rule1和Rule2为N / A, AssemblyLoadTest\\bin\\Debug\\netcoreapp2.1\\v1在Rule3扫描候选中。 When executing v2.GetValue , Rule1 is still N/A, however Rule2 is applied here (if Rule3 is applied, why exceptions?) 当执行v2.GetValue ,Rule1仍然是N / A,但是这里应用Rule2(如果应用Rule3,为什么要例外?)

Question2: Why the version is ignored even Wrapper.V2 reference ThirdPartyDependency.dll using 问题2:为什么甚至使用Wrapper.V2参考ThirdPartyDependency.dll也会忽略该版本

<Reference Include="ThirdPartyDependency, Version=2.0.0.0">
  <HintPath>..\lib\ThirdPartyDependency\2.0.0.0\ThirdPartyDependency.dll</HintPath>
</Reference> 

Great answer from Vitek Karas , original link here . Vitek Karas的很好答案,原始链接在这里

Kind of unfortunately all of the behavior you describe is currently as designed. 不幸的是,您描述的所有行为目前都是设计好的。 That doesn't mean it's intuitive (which it's totally not). 这并不意味着它是直观的(完全不是)。 Let me try to explain. 让我尝试解释一下。

Assembly binding happens based on AssemblyLoadContext (ALC). 程序集绑定基于AssemblyLoadContext(ALC)进行。 Each ALC can have only one version of any given assembly loaded (so only one assembly of a given simple name, ignoring versions, culture, keys and so on). 每个ALC只能加载任何给定程序集的一个版本(因此,只有一个给定简单名称的程序集,而忽略版本,区域性,键等)。 You can create a new ALC which then can have any assemblies loaded again, with same or different versions. 您可以创建一个新的ALC,然后可以再次加载具有相同或不同版本的任何程序集。 So ALCs provide binding isolation. 因此,ALC提供了绑定隔离。

Your .exe and related assemblies are loaded into a Default ALC - one which is created at the start of the runtime. 您的.exe和相关程序集将加载到默认ALC中-该默认ALC在运行时开始时创建。

Assembly.LoadFrom will try to load the specified file into the Default ALC - always. Assembly.LoadFrom将尝试始终将指定文件加载到默认ALC中。 Let me stress the "try" word here. 让我在这里强调“尝试”一词。 If the Default ALC already loaded assembly with the same name, and the already loaded assembly is equal or higher version, then the LoadFrom will succeed, but it will use the already loaded assembly (effectively ignoring the path you specified). 如果默认ALC已加载的程序集具有相同的名称,并且已加载的程序集等于或更高版本,则LoadFrom将成功,但是它将使用已加载的程序集(有效地忽略您指定的路径)。 If on the other hand the already loaded assembly is of a lower version then the one you're trying to load - this will fail (we can't load the same assembly for the second time into the same ALC). 另一方面,如果已经加载的程序集的版本比您要加载的程序集的版本低,这将失败(我们无法第二次将同一程序集加载到同一ALC中)。

Assembly.LoadFile will load the specified file into a new ALC - always creates a new ALC. Assembly.LoadFile会将指定的文件加载到新的ALC中-始终创建一个新的ALC。 So the load will effectively always succeed (there's no way this can collide with anything since it's in its own ALC). 因此,负载将始终有效地成功(由于它在自己的ALC中,因此不可能与任何东西发生冲突)。

So now to your scenarios: 所以现在到您的方案:

Test1 This works because your ResolveAssembly event handler loads the two assemblies into separate ALCs (LoadFile will create a new one, so the first assembly goes to the default ALC, and the second one goes into its own). Test1之所以有效,是因为您的ResolveAssembly事件处理程序将两个程序集加载到单独的ALC中(LoadFile将创建一个新的程序集,因此第一个程序集将使用默认的ALC,而第二个程序集将使用其自己的)。

Test2 This fails because LoadFrom tries to load the assembly into the Default ALC. Test2这失败,因为LoadFrom尝试将程序集加载到默认ALC中。 The failure actually occurs in the AssemblyResolve handler when it calls the second LoadFrom. 当它调用第二个LoadFrom时,该故障实际上发生在AssemblyResolve处理程序中。 First time it loaded v1 into Default, the second time it tries to load v2 into Default - which fails because Default already has v1 loaded. 第一次将v1加载到Default中,第二次尝试将v2加载到Default中,此操作失败,因为Default已经加载了v1。

Test3 This fails the same way because it internally does basically exactly what Test2 does. Test3这以相同的方式失败,因为它在内部基本上完全执行Test2的操作。 Assembly.LoadFrom also registers event handler for AssemblyResolve and that makes sure that dependent assemblies can be loaded from the same folder. Assembly.LoadFrom还为AssemblyResolve注册事件处理程序,并确保可以从同一文件夹中加载相关程序集。 So in your case v1\\Wrapper.V1.dll will resolve its dependency to v1\\ThirdPartyDependency.dll because it's next to it on the disk. 因此,在您的情况下,v1 \\ Wrapper.V1.dll会将其依赖项解析为v1 \\ ThirdPartyDependency.dll,因为它在磁盘上紧挨着它。 Then for v2 it will try to do the same, but v1 is already loaded, so it fails just like in Test2. 然后,对于v2,它将尝试执行相同的操作,但是v1已经加载,因此它就像在Test2中一样失败。 Remember that LoadFrom loads everything into the Default ALC, so collisions are possible. 请记住,LoadFrom将所有内容加载到默认ALC中,因此可能发生冲突。

Your questions: 你的问题:

Question1 LoadFile works because it loads the assembly into its own ALC, which provides full isolation and thus there are never any conflicts. Question1 LoadFile之所以有效,是因为它将程序集加载到其自己的ALC中,该ALC提供了完全隔离,因此永远不会发生任何冲突。 LoadFrom loads the assembly into the Default ALC, so if that already has assembly with the same name loaded, there might be conflicts. LoadFrom将程序集加载到默认ALC中,因此,如果该程序集已经加载了具有相同名称的程序集,则可能存在冲突。

Question2 The version is actually not ignored. Question2实际上不会忽略该版本。 The version is honored which is why Test2 and Test3 fail. 该版本很受好评,这就是为什么Test2和Test3失败的原因。 But I might not understand this question correctly - it's not clear to me in which context you're asking it. 但是我可能无法正确理解这个问题-我不清楚您要问的是哪种情况。

CLR binding order The order of Rules you describe is different. CLR绑定顺序您描述的规则的顺序不同。 It's basically: 基本上是:

  • Rule2 - if it's already loaded - use it (including if higher version is already loaded, then use that) Rule2-如果已经加载-使用它(包括如果已经加载了更高版本,则使用它)
  • Rule1 - if everything fails - as a last resort - call AppDomain.AssemblyResolve 规则1-如果一切失败-作为最后的选择-调用AppDomain.AssemblyResolve

Rule 3 actually doesn't exist. 规则3实际上不存在。 .NET Core doesn't have a notion of probing paths or code base. .NET Core没有探测路径或代码库的概念。 It sort of does for the assemblies which are statically referenced by the app, but for dynamically loaded assemblies no probing is performed (with the exception of LoadFrom loading dependent assemblies from the same folder as the parent as described above). 对于由应用程序静态引用的程序集,它确实可以执行此操作,但是对于动态加载的程序集,则不执行任何探测(如上所述,LoadFrom从与父级相同的文件夹中加载从属程序集的依赖程序集除外)。

Solutions To make this fully work, you would need to do either: 解决方案要使此功能完全起作用,您需要执行以下任一操作:

  • Use the LoadFile along with your AssemblyResolve handler. 将LoadFile与AssemblyResolve处理程序一起使用。 But the problem here is that if you LoadFile an assembly which itself has other dependencies, you will need to handle those in your handler as well (you lose the "nice" behavior of LoadFrom which loads dependencies from the same folder) 但是这里的问题是,如果您对本身具有其他依赖项的程序集进行FileFile加载,则还需要处理您的处理程序中的那些依赖项(您会失去从同一文件夹加载依赖项的LoadFrom的“好”行为)

  • Implement your own ALC which handles all dependencies. 实现自己的ALC,以处理所有依赖项。 This is technically the cleaner solution, but potentially more work. 从技术上讲,这是更清洁的解决方案,但可能需要做更多的工作。 And it's similar in that regard that you still have to implement the loading from the same folder if needed. 就这一点而言,这相似,如果需要,您仍然必须从同一文件夹实施加载。

We are actively working on making scenarios like this easy. 我们正在积极努力简化这种情况。 Today they are doable, but pretty hard. 今天,它们是可行的,但是很难。 The plan is to have something which solves this for .NET Core 3. We're also very aware of the lack of documentation/guidance in this area. 该计划是为了解决.NET Core 3的问题。我们也很清楚这方面缺乏文档/指南。 And last but not least, we are working on improving the error messages, which are currently very confusing. 最后但并非最不重要的一点是,我们正在努力改进错误消息,这些消息目前非常混乱。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 加载程序集及其依赖项 - Loading assemblies and its dependencies 通过NDepend API以编程方式获取程序集之间的依赖关系 - Getting dependencies between assemblies programatically by NDepend APIs 将具有依赖项的程序集加载到其目录中位于加载应用程序目录之外的其他程序集中? - Loading an assembly with dependencies to other assemblies within its directory which is outside the loading applications directory? 加载MDI WinForm C#.NET时的奇怪行为 - Strange behavior when loading mdi winform c# .net 在运行时手动加载程序集和依赖项(nuget 依赖项和 FileNotFoundException) - Manually loading of assemblies and dependencies at runtime (nuget dependencies & FileNotFoundException) Powershell 中奇怪的 DLL 加载行为 7 - Strange DLL loading behavior in Powershell 7 从不同的程序集动态加载类(具有自定义行为)? - Dynamically loading classes (with custom behavior) from different assemblies? Xaml ListView 以编程方式 C# 导致奇怪的行为 - Xaml ListView Programatically C# causing strange behavior 插件体系结构 - 在运行时加载程序集时出错 - Plugin architecture - Error when loading assemblies in runtime 加载外部组件时的最佳做法 - Best practise when loading foreign assemblies
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM