Сборка AppDomain не найдена при загрузке из байтового массива - PullRequest
0 голосов
/ 02 мая 2018

Пожалуйста, потерпите меня, я потратил более 30 часов, пытаясь получить эту работу - но безуспешно.

В начале моей программы я загружаю сборку (dll) в bytearray и впоследствии удаляю ее.

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

Позже в программе я создаю новый домен приложений, загружаю байтовый массив и перечисляю типы.

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: плагин, версия = 1.0.0.0, культура = нейтральная, PublicKeyToken = ноль

...

Plugins.Test

...

Теперь я хочу создать экземпляр этого типа в новом домене приложений

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

В результате этого вызова System.IO.FileNotFoundException, и я не знаю почему.

Когда я смотрю в ProcessExplorer под .NET Assemblies -> Appdomain: plugintest, я вижу, что сборка правильно загружена в новый домен приложения.

Я подозреваю, что исключение произошло, потому что сборка снова ищется на диске. Но почему программа хочет загрузить его снова?

Как я могу создать экземпляр в новом домене приложения со сборкой, загруженной из байтового массива?

Ответы [ 3 ]

0 голосов
/ 04 мая 2018

Основная проблема здесь - думать, что вы можете создать плагин во время выполнения кода в основном домене приложения.

Вместо этого вам нужно создать прокси-тип, который определен в уже загруженной сборке, но создан в домене приложения 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, это означает, что вызовы к этому объекту могут быть переданы через границы домена приложения с помощью удаленного взаимодействия.
  • При передаче данных в прокси-класс или из него данные должны быть помечены [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, это фактически сериализуется в строку и передается в новый домен приложения для выполнения. Это не обычный вызов функции, и вам нужно быть очень осторожным с тем, что вы передаете в / из этих методов.

0 голосов
/ 05 мая 2018

В результате этого вызова возникает System.IO.FileNotFoundException, и я не знаю почему. Я подозреваю, что исключение произошло, потому что сборка снова ищется на диске. Но почему программа хочет загрузить его снова?

Ключом здесь является понимание контекста загрузчика, отличная статья на MSDN:

Думайте о контекстах загрузчика как о логических контейнерах в домене приложения, в котором содержатся сборки. В зависимости от способа загрузки сборок они попадают в один из трех контекстов загрузчика.

  • Загрузка контекста
  • LoadFrom context
  • Ни один из контекстов

Загрузка из byte[] помещает сборку в контекст Ни.

Что касается контекста Ни, сборки в этом контексте не могут быть привязаны, если приложение не подписано на событие AssemblyResolve. Этот контекст, как правило, следует избегать.

В приведенном ниже коде мы используем событие AssemblyResolve для загрузки сборки в контексте Load, что позволяет нам привязываться к ней.

Как я могу создать экземпляр в новом домене приложения со сборкой, загруженной из байтового массива?

Обратите внимание, что это всего лишь подтверждение концепции, изучение гайки и болты контекстов загрузчика. Рекомендуемый подход заключается в использовании прокси, как описано в @ caesay и далее прокомментировано Сюзанной Кук в этой статье .

Вот реализация, которая не хранит ссылку на экземпляр (аналогично «забыл и забыл»).

Сначала наш плагин:

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") успешно вызван из домена приложения по умолчанию, хотя он не знает о сборке плагина.

0 голосов
/ 02 мая 2018

Вы пытались указать полное имя Ассамблеи, в вашем случае

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

...