Сборка AppDomain не найдена при загрузке из байтового массива

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

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

_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: plugin, Version=1.0.0.0, Culture=neutral,PublicKeyToken=null

...

Plugins.Test

...

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

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

Этот вызов приводит к System.IO.FileNotFoundException, и я не знаю почему.

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

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

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

Вы пытались перехватить событие AssemblyResolve AppDomain msdn.microsoft.com/fr-fr/library/… и вернуть то, что он запрашивает, «вручную»?

Simon Mourier 04.05.2018 18:04

@SimonMourier Да, я пробовал это, как описано здесь: stackoverflow.com/a/19702548/9279154. По-прежнему генерируется исключение FileNotFound.

Jenny 04.05.2018 18:11

Отвечаете ли вы ненулевой сборке на каждый вызов AssemblyResolve? Иногда вам нужно подключить как начальный домен приложения (тот, который создает новый домен приложения), так и новый домен приложения.

Simon Mourier 04.05.2018 18:39

@SimonMourier. Да, я вернул Assembly.Load (_myBytes) в обоих доменах приложений, когда попробовал этот подход.

Jenny 04.05.2018 18:53
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
7
4
1 148
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

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

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

Просто попробовал. По-прежнему генерируется исключение FileNotFoundException.

Jenny 02.05.2018 20:22

Это должен был быть комментарий

Tarun Lalwani 05.05.2018 12:24
Ответ принят как подходящий

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

Вместо этого вам нужно создать тип прокси, который определен в уже загруженной сборке, но создан в домене приложения новый. Вы не могу передаете типы через границы домена приложения без загрузки сборки типа в оба домена приложения. Например, если вы хотите перечислить типы и распечатать на консоли, как описано выше, вы должны сделать это из кода, который выполняется в новом домене приложения, а не из кода, который выполняется в текущем домене приложения..

Итак, давайте создадим наш прокси-сервер плагина, он будет существовать в вашем первичная сборка и будет отвечать за выполнение всего кода, связанного с плагином:

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

Интересный пост, хотя я бы немного перефразировал первые абзацы (см. Мой ответ).

Funk 05.05.2018 20:30

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 есть отличная статья:

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.

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

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

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 и дополнительно прокомментирован Сюзанной Кук в эта статья.

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

Во-первых, наш плагин:

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

Интересный ответ, и спасибо, что поправили меня в том, что я ошибался. Здесь важно отметить, что весь приведенный выше код выполняется в основном домене приложения, и это работает только так, как описано, потому что вы фактически создали ObjectHandle через свой вызов CreateInstance. На самом деле вы не можете запускать какие-либо методы для созданного типа, потому что он не наследуется от MarshalByRefObject. (то есть, если вы попытаетесь его развернуть, вы получите сообщение об ошибке, что ваш плагин не сериализуем)

caesay 07.05.2018 15:12

@caesay Верно, и кроме того, Assembly.Load() действительно должен вызываться из соответствующего AppDomain (как с прокси).

Funk 07.05.2018 18:03

Другие вопросы по теме