从字节数组加载时找不到 AppDomain 程序集

AppDomain Assembly not found when loaded from byte array

请耐心等待,我花了 30 多个小时来完成这项工作 - 但没有成功。

在我的程序开始时,我在 bytearray 中加载一个程序集 (dll),然后将其删除。

_myBytes = File.ReadAllBytes(@"D:\Projects\AppDomainTest\plugin.dll");

稍后在程序中我创建一个新的 Appdomain,加载字节数组并枚举类型。

var domain = AppDomain.CreateDomain("plugintest", null, null, null, false);

domain.Load(_myBytes);

foreach (var ass in domain.GetAssemblies())
{
    Console.WriteLine($"ass.FullName: {ass.FullName}");
    Console.WriteLine(string.Join(Environment.NewLine, ass.GetTypes().ToList()));
}

正确列出的类型:

ass.FullName: plugin, Version=1.0.0.0, Culture=neutral,PublicKeyToken=null

...

Plugins.Test

...

现在我想在新的 AppDomain 中创建该类型的实例

domain.CreateInstance("plugin", "Plugins.Test");

此调用结果为 System.IO.FileNotFoundException,我不知道为什么。

当我查看 .NET Assemblies -> Appdomain: plugintest 下的 ProcessExplorer 时,我发现程序集已正确加载到新的应用程序域中。

我怀疑是因为在磁盘上再次搜索程序集而出现异常。但是为什么程序要重新加载呢?

如何使用从字节数组加载的程序集在新的应用程序域中创建实例?

您是否尝试过提供程序集的全名,在您的情况下

domain.CreateInstance("plugin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "Plugins.Test");

这里的主要问题是认为您可以在主应用程序域中执行代码时实例化插件。

您需要做的是创建一个代理类型,它在已加载的程序集中定义,但在 new 应用程序域中实例化。您不能 在类型的程序集未加载到两个应用程序域的情况下跨应用程序域边界传递类型。 例如,如果你想像上面那样枚举类型并打印到控制台,你应该从在新应用程序域中执行的代码中执行,而不是从在当前应用程序域中执行的代码中执行.

那么,让我们创建我们的插件代理,它将存在于您的 主程序集 中,并将负责执行所有与插件相关的代码:

// Mark as MarshalByRefObject allows method calls to be proxied across app-domain boundaries
public class PluginRunner : MarshalByRefObject
{
    // make sure that we're loading the assembly into the correct app domain.
    public void LoadAssembly(byte[] byteArr)
    {
        Assembly.Load(byteArr);
    }

    // be careful here, only types from currently loaded assemblies can be passed as parameters / return value.
    // also, all parameters / return values from this object must be marked [Serializable]
    public string CreateAndExecutePluginResult(string assemblyQualifiedTypeName)
    {
        var domain = AppDomain.CurrentDomain;

        // we use this overload of GetType which allows us to pass in a custom AssemblyResolve function
        // this allows us to get a Type reference without searching the disk for an assembly.
        var pluginType = Type.GetType(
            assemblyQualifiedTypeName,
            (name) => domain.GetAssemblies().Where(a => a.FullName == name.FullName).FirstOrDefault(),
            null,
            true);

        dynamic plugin = Activator.CreateInstance(pluginType);

        // do whatever you want here with the instantiated plugin
        string result = plugin.RunTest();

        // remember, you can only return types which are already loaded in the primary app domain and can be serialized.
        return result;
    }
}

上面评论中的几个要点我在这里重申一下:

  • 您必须从 MarshalByRefObject 继承,这意味着可以使用远程处理跨应用程序域边界代理对该对象的调用。
  • 将数据传入或传出代理 class 时,数据必须标记为 [Serializable] 并且必须属于当前加载的程序集中的类型。如果你需要你的插件 return 一些特定的对象给你,比如 PluginResultModel 那么你应该在一个由两个 assemblies/appdomains 加载的共享程序集中定义这个 class。
  • 必须在其当前状态下将程序集限定类型名称传递给 CreateAndExecutePluginResult,但可以通过自己迭代程序集和类型并删除对 Type.GetType 的调用来删除此要求。

接下来,您需要创建域和 运行 代理:

static void Main(string[] args)
{
    var bytes = File.ReadAllBytes(@"...filepath...");
    var domain = AppDomain.CreateDomain("plugintest", null, null, null, false);
    var proxy = (PluginRunner)domain.CreateInstanceAndUnwrap(typeof(PluginRunner).Assembly.FullName, typeof(PluginRunner).FullName);
    proxy.LoadAssembly(bytes);
    proxy.CreateAndExecutePluginResult("TestPlugin.Class1, TestPlugin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null");
}

再说一遍,因为它非常重要,我很长一段时间都没有理解这一点:当你在这个代理上执行一个方法时class,比如proxy.LoadAssembly这个实际上是被序列化为一个字符串并被传递到新的应用程序域来执行。这不是正常的函数调用,您需要非常小心传递 to/from 这些方法。

This call results in System.IO.FileNotFoundException and I don't know why. I suspect the exception to occur because the assembly is searched again on disk. But why does the program want to load it again?

这里的关键是理解加载器上下文,MSDN 上有an excellent article

Think of loader contexts as logical buckets within an application domain that hold assemblies. Depending on how the assemblies were being loaded, they fall into one of three loader contexts.

  • 加载上下文
  • 从上下文加载
  • 上下文都没有

byte[] 加载会将程序集置于 Neither 上下文中。

As for the Neither context, assemblies in this context cannot be bound to, unless the application subscribes to the AssemblyResolve event. This context should generally be avoided.

在下面的代码中,我们使用 AssemblyResolve 事件在 Load 上下文中加载程序集,使我们能够绑定到它。

How can I create an instance in a new appdomain with an assembly loaded from byte array?

请注意,这只是概念验证,探索加载程序上下文的具体细节。建议的方法是 使用 @caesay 描述的代理并由 Suzanne Cook 在 this article.

中进一步评论

这是一个不保留对实例的引用的实现(类似于即发即弃)。

首先,我们的插件:

Test.cs

namespace Plugins
{
    public class Test
    {
        public Test()
        {
            Console.WriteLine($"Hello from {AppDomain.CurrentDomain.FriendlyName}.");
        }
    }
}

接下来,在一个新的 ConsoleApp 中,我们的插件加载器:

PluginLoader.cs

[Serializable]
class PluginLoader
{
    private readonly byte[] _myBytes;
    private readonly AppDomain _newDomain;

    public PluginLoader(byte[] rawAssembly)
    {
        _myBytes = rawAssembly;
        _newDomain = AppDomain.CreateDomain("New Domain");
        _newDomain.AssemblyResolve += new ResolveEventHandler(MyResolver);
    }

    public void Test()
    {
        _newDomain.CreateInstance("plugin", "Plugins.Test");
    }

    private Assembly MyResolver(object sender, ResolveEventArgs args)
    {
        AppDomain domain = (AppDomain)sender;
        Assembly asm = domain.Load(_myBytes);
        return asm;
    }
}

Program.cs

class Program
{
    static void Main(string[] args)
    {
        byte[] rawAssembly = File.ReadAllBytes(@"D:\Projects\AppDomainTest\plugin.dll");
        PluginLoader plugin = new PluginLoader(rawAssembly);

        // Output: 
        // Hello from New Domain
        plugin.Test();

        // Output: 
        // Assembly: mscorlib
        // Assembly: ConsoleApp
        foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
        {
            Console.WriteLine($"Assembly: {asm.GetName().Name}");
        }

        Console.ReadKey();
    }
}

输出显示 CreateInstance("plugin", "Plugins.Test") 已从默认应用程序域成功调用,尽管它不知道插件程序集。