Як завантажувати збірку в 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, Версія = 1.0.0.0, Культура = нейтральна, 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 або папку бін поточної програми. Використовуйте LoadFileдля завантаження довільного збірного файлу, але зауважте, що якщо ви це зробите, вам потрібно буде самостійно завантажити будь-які залежності.


20
Перевірте код, який я написав, щоб вирішити цю проблему: github.com/jduv/AppDomainToolkit . Зокрема, подивіться на метод LoadAssemblyWithReferences у цьому класі: github.com/jduv/AppDomainToolkit/blob/master/AppDomainToolkit/…
Jduv

3
Я виявив, що, хоча це працює більшу частину часу, в деяких випадках вам все-таки потрібно приєднати обробник до AppDomain.CurrentDomain.AssemblyResolveподії, як описано в цій відповіді MSDN . У моєму випадку я намагався підключити розгортання SpecRun, що працює під MSTest, але я думаю, що він застосовується до багатьох ситуацій, коли ваш код може не працювати з "первинного" AppDomain - розширення VS, MSTest тощо.
Aaronaught

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

@Jduv Я б схвалив ваш коментар приблизно 100 разів, якби міг. Ваша бібліотека допомогла мені вирішити, здавалося б, нерозв'язну проблему, з якою я стикався з динамічним завантаженням збірок під MSBuild. Ви повинні рекламувати його до відповіді!
Філіп Даніельс

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
Як це рішення вкладається в контекст створення нового AppDomain, хтось може пояснити?
Tri Q Tran

2
А MarshalByRefObjectможе бути передано навколо додатків. Тож я б здогадався, що Assembly.LoadFromнамагається завантажити збірку в новий додаток, що тільки можливо, якщо об'єкт, що викликає, міг бути переданий між цими додатками. Це також називається видаленням, як описано тут: msdn.microsoft.com/en-us/library/…
Крістоф Мейснер

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);
        }
    }
}

Якщо вам потрібно завантажити збірки з папки, яка відрізняється від поточної папки домену додатка, створіть новий домен додатка з певною папкою шляху пошуку dlls.

Наприклад, рядок створення домену додатка з наведеного вище коду слід замінити на:

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

Таким чином, всі dlls будуть автоматично вирішені з dllsSearchPath.


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

Ви використовуєте клас проксі, щоб уникнути завантаження нової збірки у домен вашого абонента. Якщо ви будете використовувати Assembly.LoadFrom (string), домен абонента намагатиметься завантажити нові посилання на збірку і не знайде їх, оскільки він не шукає збірок у "[AsmPath]". ( msdn.microsoft.com/en-us/library/yx7xezcf%28v=vs.110%29.aspx )
Nir

11

На новому AppDomain спробуйте встановити обробник подій AssemblyResolve . Ця подія називається, коли залежність відсутня.


Це не так. Насправді ви отримуєте виняток у рядку, де ви реєструєте цю подію в новому AppDomain. Ви повинні зареєструвати цю подію на поточному AppDomain.
користувач1004959

Це робиться, якщо клас успадковується від MarshalByRefObject. Це не так, якщо клас позначений лише атрибутом [Serializable].
користувач2126375

5

Вам потрібно обробити події AppDomain.AssemblyResolve або AppDomain.ReflectionOnlyAssemblyResolve (залежно від того, яке завантаження ви виконуєте) у випадку, якщо згадана збірка не знаходиться в GAC або на шляху зондування CLR.

AppDomain.AssemblyResolve

AppDomain.ReflectionOnlyAssemblyResolve


Отже, я повинен вказати запитувану збірку вручну? Навіть це є в новій AppBase AppDomain? Чи є спосіб цього не зробити?
абатищев

5

Щоб зрозуміти відповідь @ user1996230, тож я вирішив надати більш чіткий приклад. У наведеному нижче прикладі я роблю проксі-сервер для об'єкта, завантаженого в інший AppDomain, і викликаю метод цього об’єкта з іншого домену.

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"});
}

Деякі невеликі помилки в коді, і я повинен визнати, що я не вірив, що це спрацює, але це була безпека для мене. Дякую тонну.
Оуен

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.

Цей проект, як і клієнтський proj, посилається на proxy proj, і ви реалізуєте інтерфейс.

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введеній змінній, а потім просто зателефонуйте членам на цю адресу безпосередньо.

Там у вас є.

Це дозволяє завантажити збірку, на яку ваш клієнтський proj не посилається окремо, 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.