Как рекурсивно загрузить сборку в AppDomain со всеми ссылками?


113

Я хочу загрузить новую AppDomainсборку со сложным деревом ссылок (MyDll.dll -> Microsoft.Office.Interop.Excel.dll -> Microsoft.Vbe.Interop.dll -> Office.dll -> stdole.dll)

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

string dir = @"SomePath"; // different from AppDomain.CurrentDomain.BaseDirectory
string path = System.IO.Path.Combine(dir, "MyDll.dll");

AppDomainSetup setup = AppDomain.CurrentDomain.SetupInformation;
setup.ApplicationBase = dir;
AppDomain domain = AppDomain.CreateDomain("SomeAppDomain", null, setup);

domain.Load(AssemblyName.GetAssemblyName(path));

и получил FileNotFoundException:

Не удалось загрузить файл или сборку MyDll, Version = 1.0.0.0, Culture = нейтральный, PublicKeyToken = null или одну из их зависимостей. Система не может найти указанный файл.

Я думаю, что ключевая часть - это одна из его зависимостей .

Хорошо, я сделаю следующий раньше domain.Load(AssemblyName.GetAssemblyName(path));

foreach (AssemblyName refAsmName in Assembly.ReflectionOnlyLoadFrom(path).GetReferencedAssemblies())
{
    domain.Load(refAsmName);
}

Но попал FileNotFoundExceptionснова, на другой (упомянутой) сборке.

Как загрузить все ссылки рекурсивно?

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


1
Я загружал подобные сборки много раз раньше, мне никогда не приходилось вручную загружать все ссылки. Я не уверен, что посылка этого вопроса верна.
Мик

Ответы:


68

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

 class Program
{
    static void Main(string[] args)
    {
        AppDomainSetup domaininfo = new AppDomainSetup();
        domaininfo.ApplicationBase = System.Environment.CurrentDirectory;
        Evidence adevidence = AppDomain.CurrentDomain.Evidence;
        AppDomain domain = AppDomain.CreateDomain("MyDomain", adevidence, domaininfo);

        Type type = typeof(Proxy);
        var value = (Proxy)domain.CreateInstanceAndUnwrap(
            type.Assembly.FullName,
            type.FullName);

        var assembly = value.GetAssembly(args[0]);
        // AppDomain.Unload(domain);
    }
}

public class Proxy : MarshalByRefObject
{
    public Assembly GetAssembly(string assemblyPath)
    {
        try
        {
            return Assembly.LoadFile(assemblyPath);
        }
        catch (Exception)
        {
            return null;
            // throw new InvalidOperationException(ex);
        }
    }
}

Также обратите внимание, что при использовании LoadFromвы, скорее всего, получите FileNotFoundисключение, потому что распознаватель сборок попытается найти сборку, которую вы загружаете, в GAC или в папке bin текущего приложения. LoadFileВместо этого используйте для загрузки произвольного файла сборки, но обратите внимание, что если вы это сделаете, вам придется загружать любые зависимости самостоятельно.


20
Ознакомьтесь с кодом, который я написал для решения этой проблемы: github.com/jduv/AppDomainToolkit . В частности, посмотрите на метод LoadAssemblyWithReferences в этом классе: github.com/jduv/AppDomainToolkit/blob/master/AppDomainToolkit/…
Jduv

3
Я обнаружил, что, хотя это работает большую часть времени, в некоторых случаях вам все же нужно прикрепить обработчик к AppDomain.CurrentDomain.AssemblyResolveсобытию, как описано в этом ответе MSDN . В моем случае я пытался подключиться к развертыванию SpecRun, работающему под MSTest, но я думаю, что это применимо ко многим ситуациям, в которых ваш код может не запускаться из «основного» домена приложения - расширения VS, MSTest и т. Д.
Aaronaught,

Ах интересно. Я изучу это и посмотрю, смогу ли я сделать это немного проще для работы через ADT. Извините, что код уже давно умер - у всех нас есть дневная работа :).
Jduv

@Jduv Проголосовал бы за ваш комментарий примерно 100 раз, если бы мог. Ваша библиотека помогла мне решить, казалось бы, неразрешимую проблему с динамической загрузкой сборки в MSBuild. Вы должны довести это до ответа!
Philip Daniels

2
@Jduv вы уверены, что эта assemblyпеременная будет ссылаться на сборку из "MyDomain"? Я думаю, var assembly = value.GetAssembly(args[0]);вы загрузите ваш файл args[0]в оба домена, и assemblyпеременная будет ссылаться на копию из основного домена приложения
Игорь Бендруп

14

http://support.microsoft.com/kb/837908/en-us

Версия C #:

Создайте класс модератора и унаследуйте его от MarshalByRefObject:

class ProxyDomain : MarshalByRefObject
{
    public Assembly GetAssembly(string assemblyPath)
    {
        try
        {
            return Assembly.LoadFrom(assemblyPath);
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException(ex.Message);
        }
    }
}

звонок с сайта клиента

ProxyDomain pd = new ProxyDomain();
Assembly assembly = pd.GetAssembly(assemblyFilePath);

6
Может кто-нибудь объяснить, как это решение помещается в контекст создания нового домена приложений?
Tri Q Tran

2
A MarshalByRefObjectможно передавать по доменам приложений. Поэтому я бы предположил, что он Assembly.LoadFromпытается загрузить сборку в новый домен приложения, что возможно только в том случае, если вызывающий объект может быть передан между этими доменами приложений. Это также называется удаленным
Christoph Meißner,

32
Это не работает. Если вы выполните код и проверите AppDomain.CurrentDomain.GetAssemblies (), вы увидите, что целевая сборка, которую вы пытаетесь загрузить, загружается в текущий домен приложения, а не в прокси- домен .
Jduv

41
Это полная чушь. Наследование от MarshalByRefObjectне волшебным образом заставляет его загружаться во всех других AppDomain, оно просто сообщает платформе .NET создать прозрачный прокси удаленного взаимодействия вместо использования сериализации, когда вы разворачиваете ссылку из одного AppDomainв другой AppDomain(типичный способ - это CreateInstanceAndUnwrapметод). Не могу поверить, что у этого ответа более 30 голосов; код здесь просто бессмысленно обходной способ вызова Assembly.LoadFrom.
Aaronaught

1
Да, это похоже на полную чушь, но он получил 28 голосов и помечен как ответ. В предоставленной ссылке даже не упоминается MarshalByRefObject. Довольно странно. Если это на самом деле что-то делает, я бы хотел, чтобы кто-нибудь объяснил, как
Мик

12

Как только вы передадите экземпляр сборки обратно в вызывающий домен, вызывающий домен попытается загрузить его! Вот почему вы получаете исключение. Это происходит в вашей последней строке кода:

domain.Load(AssemblyName.GetAssemblyName(path));

Таким образом, все, что вы хотите сделать со сборкой, следует делать в прокси-классе - классе, который наследует MarshalByRefObject .

Примите во внимание, что и вызывающий домен, и новый созданный домен должны иметь доступ к сборке прокси-класса. Если ваша проблема не слишком сложна, подумайте о том, чтобы оставить папку ApplicationBase без изменений, чтобы она была такой же, как и папка домена вызывающего абонента (новый домен будет загружать только необходимые ему сборки).

В простом коде:

public void DoStuffInOtherDomain()
{
    const string assemblyPath = @"[AsmPath]";
    var newDomain = AppDomain.CreateDomain("newDomain");
    var asmLoaderProxy = (ProxyDomain)newDomain.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, typeof(ProxyDomain).FullName);

    asmLoaderProxy.GetAssembly(assemblyPath);
}

class ProxyDomain : MarshalByRefObject
{
    public void GetAssembly(string AssemblyPath)
    {
        try
        {
            Assembly.LoadFrom(AssemblyPath);
            //If you want to do anything further to that assembly, you need to do it here.
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException(ex.Message, ex);
        }
    }
}

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

Например, строку создания домена приложения из приведенного выше кода следует заменить на:

var dllsSearchPath = @"[dlls search path for new app domain]";
AppDomain newDomain = AppDomain.CreateDomain("newDomain", new Evidence(), dllsSearchPath, "", true);

Таким образом, все библиотеки DLL будут автоматически разрешены из dllsSearchPath.


Почему мне нужно загружать сборку с помощью прокси-класса? В чем разница по сравнению с загрузкой с помощью Assembly.LoadFrom (string). Меня интересуют технические детали с точки зрения CLR. Буду очень признателен, если вы ответите.
Деннис Кассель,

Вы используете прокси-класс, чтобы избежать загрузки новой сборки в ваш вызывающий домен. Если вы будете использовать Assembly.LoadFrom (строка), вызывающий домен попытается загрузить новые ссылки на сборки и не найдет их, потому что он не ищет сборки в «[AsmPath]». ( Msdn.microsoft.com/en-us/library/yx7xezcf%28v=vs.110%29.aspx )
Нир

11

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


Это не так. Фактически, вы получаете исключение в строке, в которой регистрируете это событие в новом домене приложения. Вы должны зарегистрировать это событие в текущем домене приложений.
user1004959 02

Это происходит, если класс унаследован от MarshalByRefObject. Это не так, если класс отмечен только атрибутом [Serializable].
user2126375

5

Вам необходимо обработать события AppDomain.AssemblyResolve или AppDomain.ReflectionOnlyAssemblyResolve (в зависимости от того, какую нагрузку вы выполняете), если указанная сборка не находится в GAC или на пути проверки CLR.

AppDomain.AssemblyResolve

AppDomain.ReflectionOnlyAssemblyResolve


Значит, мне нужно вручную указывать запрашиваемую сборку? Даже в AppBase нового AppDomain? Есть ли способ этого не делать?
Абатищев

5

Мне потребовалось время, чтобы понять ответ @ user1996230, поэтому я решил привести более подробный пример. В приведенном ниже примере я создаю прокси для объекта, загруженного в другом домене приложения, и вызываю метод этого объекта из другого домена.

class ProxyObject : MarshalByRefObject
{
    private Type _type;
    private Object _object;

    public void InstantiateObject(string AssemblyPath, string typeName, object[] args)
    {
        assembly = Assembly.LoadFrom(AppDomain.CurrentDomain.BaseDirectory + AssemblyPath); //LoadFrom loads dependent DLLs (assuming they are in the app domain's base directory
        _type = assembly.GetType(typeName);
        _object = Activator.CreateInstance(_type, args); ;
    }

    public void InvokeMethod(string methodName, object[] args)
    {
        var methodinfo = _type.GetMethod(methodName);
        methodinfo.Invoke(_object, args);
    }
}

static void Main(string[] args)
{
    AppDomainSetup setup = new AppDomainSetup();
    setup.ApplicationBase = @"SomePathWithDLLs";
    AppDomain domain = AppDomain.CreateDomain("MyDomain", null, setup);
    ProxyObject proxyObject = (ProxyObject)domain.CreateInstanceFromAndUnwrap(typeof(ProxyObject).Assembly.Location,"ProxyObject");
    proxyObject.InstantiateObject("SomeDLL","SomeType", new object[] { "someArgs});
    proxyObject.InvokeMethod("foo",new object[] { "bar"});
}

Несколько мелких опечаток в коде, и я должен признать, что не верил, что это сработает, но это спасло мне жизнь. Благодаря тонну.
Owen Ivory

4

Ключ - это событие AssemblyResolve, вызванное AppDomain.

[STAThread]
static void Main(string[] args)
{
    fileDialog.ShowDialog();
    string fileName = fileDialog.FileName;
    if (string.IsNullOrEmpty(fileName) == false)
    {
        AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
        if (Directory.Exists(@"c:\Provisioning\") == false)
            Directory.CreateDirectory(@"c:\Provisioning\");

        assemblyDirectory = Path.GetDirectoryName(fileName);
        Assembly loadedAssembly = Assembly.LoadFile(fileName);

        List<Type> assemblyTypes = loadedAssembly.GetTypes().ToList<Type>();

        foreach (var type in assemblyTypes)
        {
            if (type.IsInterface == false)
            {
                StreamWriter jsonFile = File.CreateText(string.Format(@"c:\Provisioning\{0}.json", type.Name));
                JavaScriptSerializer serializer = new JavaScriptSerializer();
                jsonFile.WriteLine(serializer.Serialize(Activator.CreateInstance(type)));
                jsonFile.Close();
            }
        }
    }
}

static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
    string[] tokens = args.Name.Split(",".ToCharArray());
    System.Diagnostics.Debug.WriteLine("Resolving : " + args.Name);
    return Assembly.LoadFile(Path.Combine(new string[]{assemblyDirectory,tokens[0]+ ".dll"}));
}

0

Мне приходилось делать это несколько раз, и я исследовал множество различных решений.

Решение, которое я нахожу наиболее элегантным и легким в реализации, может быть реализовано как таковое.

1. Создайте проект, в котором можно создать простой интерфейс.

интерфейс будет содержать подписи всех участников, которым вы хотите позвонить.

public interface IExampleProxy
{
    string HelloWorld( string name );
}

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

2. Теперь создайте проект с кодом, который вы хотите загрузить отдельно. AppDomain .

Этот проект, как и клиентский, будет ссылаться на прокси-сервер, и вы реализуете интерфейс.

public interface Example : MarshalByRefObject, IExampleProxy
{
    public string HelloWorld( string name )
    {
        return $"Hello '{ name }'";
    }
}

3. Затем в клиентском проекте загрузите код в другой AppDomain .

Итак, теперь мы создаем новый AppDomain. Можно указать базовое расположение для ссылок на сборки. Зондирование проверит наличие зависимых сборок в GAC, в текущем каталоге и в AppDomainбазовом местоположении.

// set up domain and create
AppDomainSetup domaininfo = new AppDomainSetup
{
    ApplicationBase = System.Environment.CurrentDirectory
};

Evidence adevidence = AppDomain.CurrentDomain.Evidence;

AppDomain exampleDomain = AppDomain.CreateDomain("Example", adevidence, domaininfo);

// assembly ant data names
var assemblyName = "<AssemblyName>, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null|<keyIfSigned>";
var exampleTypeName = "Example";

// Optional - get a reflection only assembly type reference
var @type = Assembly.ReflectionOnlyLoad( assemblyName ).GetType( exampleTypeName ); 

// create a instance of the `Example` and assign to proxy type variable
IExampleProxy proxy= ( IExampleProxy )exampleDomain.CreateInstanceAndUnwrap( assemblyName, exampleTypeName );

// Optional - if you got a type ref
IExampleProxy proxy= ( IExampleProxy )exampleDomain.CreateInstanceAndUnwrap( @type.Assembly.Name, @type.Name );    

// call any members you wish
var stringFromOtherAd = proxy.HelloWorld( "Tommy" );

// unload the `AppDomain`
AppDomain.Unload( exampleDomain );

если вам нужно, существует масса различных способов загрузить сборку. С этим решением вы можете использовать другой способ. Если у вас есть полное имя сборки, мне нравится использовать, CreateInstanceAndUnwrapпоскольку он загружает байты сборки, а затем создает для вас экземпляр вашего типа и возвращает то, objectчто вы можете просто преобразовать в свой тип прокси или, если вы этого не сделаете, в строго типизированный код, который вы могли бы используйте среду выполнения динамического языка и назначьте возвращаемый объект dynamicтипизированной переменной, а затем просто вызовите элементы для этого напрямую.

Вот и все.

Это позволяет загрузить сборку, на которую ваш клиентский проект не ссылается, в отдельном AppDomain и вызывать ее элементы из клиента.

Для тестирования мне нравится использовать окно «Модули» в Visual Studio. Он покажет вам домен вашей клиентской сборки и все модули, загруженные в этом домене, а также ваш новый домен приложения и какие сборки или модули загружены в этом домене.

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

`MarshalByRefObject позволит вам настроить время жизни домена, в котором он находится. Например, вы хотите, чтобы домен был уничтожен, если прокси не был вызван в течение 20 минут.

Надеюсь, это поможет.


Привет, если я правильно помню, основная проблема заключалась в том, как рекурсивно загружать все зависимости, отсюда и вопрос. Пожалуйста, проверьте свой код, изменив HelloWorld, чтобы он возвращал класс типа, Foo, FooAssemblyкоторый имеет свойство типаBar, BarAssembly , т.е. всего 3 сборки. Будет ли он продолжать работать?
абатищев

Да, нужен правильный каталог, указанный на этапе проверки сборки. В AppDomain есть ApplicationBase, но я его не тестировал. Также в файлах конфигурации вы можете указать каталоги проверки сборки, такие как app.config, который может использовать DLL, а также просто установите для копирования в свойствах. Кроме того, если у вас есть контроль над созданием сборки, которую нужно загрузить в отдельный домен приложения, ссылки могут получить HintPath, в котором указано, что нужно искать. Если бы все это не помогло, я бы подписался на новое событие AppDomains AssemblyResolve и вручную загружал сборки. Тонны примеров для этого.
SimperT
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.