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

6

请耐心等待,我花了30多个小时尝试让这个工作正常运行-但是没有成功。

在我的程序开始时,我将一个Assembly(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,我不知道原因。

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

我怀疑异常出现是因为程序又在磁盘上搜索了一遍该程序集。但为什么程序要再次加载它呢?

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


1
你尝试过挂钩AppDomain的AssemblyResolve事件https://msdn.microsoft.com/fr-fr/library/system.appdomain.assemblyresolve.aspx并手动返回它所请求的内容吗? - Simon Mourier
@SimonMourier 是的,我已经按照这里描述的方式尝试了:https://dev59.com/UWXWa4cB1Zd3GeqPKjNG#19702548。仍然会抛出FileNotFound异常。 - Jenny
你是否对每个调用AssemblyResolve的非空程序集都进行回答?有时,您需要挂钩启动应用程序域(创建新AppDomain的应用程序域)和新的AppDomain。 - Simon Mourier
@SimonMourier 是的,当我尝试那种方法时,在两个应用程序域中我都返回了Assembly.Load(_myBytes)。 - Jenny
3个回答

9
这里的主要问题是认为可以在执行主应用程序域中的代码时实例化插件。
相反,您需要创建一个代理类型,该类型在已加载的程序集中定义,但在新的应用程序域中实例化。您不能跨应用程序域边界传递类型,除非该类型的程序集在两个应用程序域中都加载了。例如,如果您想枚举类型并像上面那样打印到控制台,则应从在新应用程序域中执行的代码中执行,而不是从在当前应用程序域中执行的代码中执行。
因此,让我们创建我们的插件代理,这将存在于您的主要程序集中,并负责执行所有插件相关的代码:
// 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继承,这意味着可以通过远程调用将对此对象的调用代理到应用程序域边界。
  • 当将数据传递到代理类或从代理类传递数据时,数据必须标记为[Serializable],并且还必须是当前加载的程序集中的类型。如果您需要插件向您返回特定对象,例如PluginResultModel,则应在由两个程序集/应用程序域加载的共享程序集中定义此类。
  • 必须将一个程序集限定的类型名称传递给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");
}

我要再说一遍这个非常重要的事情,因为我很长一段时间都没有理解:当你在这个代理类上执行一个方法,比如proxy.LoadAssembly,实际上这个方法会被序列化成一个字符串,并传递到新的应用程序域中执行。这不是一个普通的函数调用,你需要非常小心地传递和从这些方法中返回的内容。


有趣的帖子,不过我会稍微改一下第一段(参见我的回答)。 - Funk

3
这个调用导致了 System.IO.FileNotFoundException 异常,我不知道为什么。我怀疑异常出现是因为程序在磁盘上再次搜索程序集。但是为什么程序要再次加载它呢?
关键在于理解装载器上下文,MSDN 上有一篇优秀的文章
将装配件保存在应用程序域内的逻辑存储桶中,取决于装配件的加载方式,它们分别属于以下三种装载器上下文之一。
- Load 上下文 - LoadFrom 上下文 - Neither 上下文
byte[] 加载将程序集置于 Neither 上下文中。
至于 Neither 上下文,除非应用程序订阅 AssemblyResolve 事件,否则无法绑定到此上下文中的程序集。通常应避免使用此上下文。
在下面的代码中,我们使用 AssemblyResolve 事件将程序集加载到 Load 上下文中,使我们能够绑定到它。
如何使用从字节数组加载的程序集在新的应用程序域中创建实例?
请注意,这仅是一个概念验证,探索装载器上下文的细节。建议的方法是像@caesay描述的那样使用代理,并由Suzanne Cook在this article中进一步评论。
这是一个不保留对实例引用的实现(类似于“fire-and-forget”)。
首先,我们的插件:

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"),尽管它没有关于插件程序集的任何知识。

1
有趣的回答,感谢您纠正我错误的地方。这里需要注意的是,上面的所有代码都在主应用程序域中运行,而这只能按照描述的方式工作,因为您实际上通过调用CreateInstance创建了一个ObjectHandle。您无法在实例化的类型上运行任何方法,因为它不继承自MarshalByRefObject。(即,如果您尝试取消包装它,您将收到一个错误,指出您的插件不可序列化) - caesay
1
@caesay 没错,除此之外,Assembly.Load() 应该从相关的 AppDomain 中调用(就像代理一样)。 - Funk

0

你尝试过提供程序集的完整名称吗?在你的情况下:

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


刚试了一下,仍然抛出FileNotFoundException异常。 - Jenny
2
这应该是一条注释。 - Tarun Lalwani

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接