Пожалуйста, потерпите меня, я потратил более 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
, я вижу, что сборка правильно загружена в новый домен приложения.
Я подозреваю, что исключение произошло из-за того, что сборка снова ищется на диске. Но почему программа хочет снова его загрузить?
Как я могу создать экземпляр в новом домене приложения со сборкой, загруженной из байтового массива?
@SimonMourier Да, я пробовал это, как описано здесь: stackoverflow.com/a/19702548/9279154. По-прежнему генерируется исключение FileNotFound.
Отвечаете ли вы ненулевой сборке на каждый вызов AssemblyResolve? Иногда вам нужно подключить как начальный домен приложения (тот, который создает новый домен приложения), так и новый домен приложения.
@SimonMourier. Да, я вернул Assembly.Load (_myBytes) в обоих доменах приложений, когда попробовал этот подход.
Вы пытались указать полное имя сборок, в вашем случае
domain.CreateInstance("plugin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "Plugins.Test");
Просто попробовал. По-прежнему генерируется исключение FileNotFoundException.
Это должен был быть комментарий
Основная проблема здесь заключается в том, что вы думаете, что вы можете создать экземпляр плагина во время выполнения кода в своем основном домене приложения.
Вместо этого вам нужно создать тип прокси, который определен в уже загруженной сборке, но создан в домене приложения новый. Вы не могу передаете типы через границы домена приложения без загрузки сборки типа в оба домена приложения. Например, если вы хотите перечислить типы и распечатать на консоли, как описано выше, вы должны сделать это из кода, который выполняется в новом домене приложения, а не из кода, который выполняется в текущем домене приложения..
Итак, давайте создадим наш прокси-сервер плагина, он будет существовать в вашем первичная сборка и будет отвечать за выполнение всего кода, связанного с плагином:
// 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 домен, который нужно выполнить. Это не обычный вызов функции, и вам нужно очень внимательно относиться к тому, что вы передаете этим методам и из них.
Интересный пост, хотя я бы немного перефразировал первые абзацы (см. Мой ответ).
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.
Загрузка из 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 Верно, и кроме того, Assembly.Load()
действительно должен вызываться из соответствующего AppDomain
(как с прокси).
Вы пытались перехватить событие AssemblyResolve AppDomain msdn.microsoft.com/fr-fr/library/… и вернуть то, что он запрашивает, «вручную»?