Jak załadować zestaw do AppDomain ze wszystkimi odwołaniami rekurencyjnie?

113

Chcę załadować do nowego AppDomainzestawu, który ma złożone drzewo odwołań (MyDll.dll -> Microsoft.Office.Interop.Excel.dll -> Microsoft.Vbe.Interop.dll -> Office.dll -> stdole.dll)

O ile zrozumiałem, kiedy ładowany jest zestaw AppDomain, jego odwołania nie byłyby ładowane automatycznie i muszę je ładować ręcznie. Więc kiedy to zrobię:

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

i otrzymałem FileNotFoundException:

Nie można załadować pliku lub zestawu „MyDll, Version = 1.0.0.0, Culture = neutral, PublicKeyToken = null” lub jednej z jego zależności. System nie może odnaleźć określonego pliku.

Myślę, że kluczową częścią jest jedna z jego zależności .

Ok, zrobię to wcześniej domain.Load(AssemblyName.GetAssemblyName(path));

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

Ale dostałem FileNotFoundExceptionponownie, na innym (wymienionym) zestawie.

Jak rekurencyjnie załadować wszystkie odwołania?

Czy muszę utworzyć drzewo odwołań przed załadowaniem zestawu głównego? Jak uzyskać odwołania do zestawu bez ładowania go?

abatishchev
źródło
1
Takie zestawy ładowałem już wiele razy, nigdy nie musiałem ręcznie ładować wszystkich ich odwołań. Nie jestem pewien, czy przesłanka tego pytania jest słuszna.
Mick,

Odpowiedzi:

68

Musisz wywołać, CreateInstanceAndUnwrapzanim obiekt proxy zostanie wykonany w obcej domenie aplikacji.

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

Należy również zauważyć, że jeśli użyjesz LoadFrom, prawdopodobnie otrzymasz FileNotFoundwyjątek, ponieważ program rozpoznawania zestawu spróbuje znaleźć zestaw, który ładujesz w GAC lub w folderze bin bieżącej aplikacji. Użyj LoadFilezamiast tego do załadowania dowolnego pliku zestawu - ale pamiętaj, że jeśli to zrobisz, będziesz musiał samodzielnie załadować wszelkie zależności.

Jduv
źródło
20
Sprawdź kod, który napisałem, aby rozwiązać ten problem: github.com/jduv/AppDomainToolkit . W szczególności spójrz na metodę LoadAssemblyWithReferences w tej klasie: github.com/jduv/AppDomainToolkit/blob/master/AppDomainToolkit/ ...
Jduv
3
Zauważyłem, że chociaż to działa przez większość czasu, w niektórych przypadkach nadal musisz dołączyć procedurę obsługi do AppDomain.CurrentDomain.AssemblyResolvezdarzenia, jak opisano w tej odpowiedzi MSDN . W moim przypadku próbowałem podłączyć się do wdrożenia SpecRun działającego pod MSTest, ale myślę, że ma to zastosowanie do wielu sytuacji, w których Twój kod może nie działać z „podstawowej” domeny AppDomain - rozszerzenia VS, MSTest itp.
Aaronaught
Ach, ciekawe. Przyjrzę się temu i zobaczę, czy mogę trochę ułatwić pracę za pośrednictwem ADT. Przepraszam, że kod jest trochę martwy od jakiegoś czasu - wszyscy mamy codzienną pracę :).
Jduv
@Jduv Głosowałbym za twoim komentarzem około 100 razy, gdybym mógł. Twoja biblioteka pomogła mi rozwiązać pozornie nierozwiązywalny problem, który miałem z dynamicznym ładowaniem zestawu w programie MSBuild. Powinieneś promować to jako odpowiedź!
Philip Daniels
2
@Jduv czy jesteś pewien, że assemblyzmienna będzie odwoływać się do zestawu z „MyDomain”? Myślę, że przez var assembly = value.GetAssembly(args[0]);Ciebie załadujesz swoje args[0]do obu domen, a assemblyzmienna odniesie kopię z głównej domeny aplikacji
Igor Bendrup
14

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

Wersja C #:

Utwórz klasę moderatora i odziedzicz ją z MarshalByRefObject:

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

zadzwoń ze strony klienta

ProxyDomain pd = new ProxyDomain();
Assembly assembly = pd.GetAssembly(assemblyFilePath);
rockvista
źródło
6
W jaki sposób to rozwiązanie wpisuje się w kontekst tworzenia nowej domeny AppDomain, czy ktoś może wyjaśnić?
Tri Q Tran
2
A MarshalByRefObjectmożna przekazywać między domenami aplikacji. Więc domyślam się, że Assembly.LoadFrompróbuje załadować zestaw w nowej domenie aplikacji, co jest możliwe tylko wtedy, gdy obiekt wywołujący mógłby zostać przekazany między tymi domenami aplikacji. Nazywa się to również zdalnym działaniem, jak opisano tutaj: msdn.microsoft.com/en-us/library/…
Christoph Meißner
32
To nie działa. Jeśli wykonasz kod i sprawdzisz AppDomain.CurrentDomain.GetAssemblies (), zobaczysz, że zestaw docelowy, który próbujesz załadować, jest ładowany do bieżącej domeny aplikacji, a nie serwera proxy.
Jduv
41
To kompletny nonsens. Dziedziczenie z MarshalByRefObjectnie powoduje magicznego ładowania wszystkich innych AppDomain, po prostu nakazuje platformie .NET utworzenie przezroczystego zdalnego proxy zamiast korzystania z serializacji podczas rozpakowywania referencji z jednego AppDomainw drugim AppDomain(typowym sposobem jest CreateInstanceAndUnwrapmetoda). Nie mogę uwierzyć, że ta odpowiedź ma ponad 30 głosów za; kod tutaj tylko bezcelowy okrężny sposób dzwonienia Assembly.LoadFrom.
Aaronaught
1
Tak, wygląda na kompletny nonsens, ale ma 28 głosów pozytywnych i jest oznaczony jako odpowiedź. Podany link nie wspomina nawet o MarshalByRefObject. Całkiem dziwaczne. Jeśli to rzeczywiście coś robi, chciałbym, żeby ktoś wyjaśnił, jak to zrobić
Mick,
12

Po przekazaniu wystąpienia zestawu z powrotem do domeny wywołującego domena wywołującego spróbuje go załadować! Dlatego otrzymujesz wyjątek. Dzieje się to w ostatniej linii kodu:

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

Zatem cokolwiek chcesz zrobić z zestawem, powinno być zrobione w klasie proxy - klasie, która dziedziczy MarshalByRefObject .

Weź pod uwagę, że domena wywołująca i nowo utworzona domena powinny mieć dostęp do zestawu klasy proxy. Jeśli problem nie jest zbyt skomplikowany, rozważ pozostawienie niezmienionego folderu ApplicationBase, aby był taki sam jak folder domeny wywołującej (nowa domena będzie ładować tylko zestawy, których potrzebuje).

W prostym kodzie:

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

Jeśli musisz załadować zestawy z folderu, który jest inny niż bieżący folder domeny aplikacji, Utwórz nową domenę aplikacji z określonym folderem ścieżki wyszukiwania bibliotek dll.

Na przykład wiersz tworzenia domeny aplikacji z powyższego kodu powinien zostać zastąpiony przez:

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

W ten sposób wszystkie biblioteki dll zostaną automatycznie rozwiązane z dllsSearchPath.

Nir
źródło
Dlaczego muszę ładować zestaw przy użyciu klasy proxy? Jaka jest różnica w porównaniu z ładowaniem go przy użyciu Assembly.LoadFrom (string). Interesują mnie szczegóły techniczne z punktu widzenia CLR. Byłbym bardzo wdzięczny, gdybyś mógł udzielić odpowiedzi.
Dennis Kassel
Używasz klasy proxy, aby uniknąć ładowania nowego zestawu do domeny wywołującej. Jeśli użyjesz Assembly.LoadFrom (string), domena wywołująca spróbuje załadować nowe odwołania do zestawu i nie znajdzie ich, ponieważ nie wyszukuje zestawów w „[AsmPath]”. ( msdn.microsoft.com/en-us/library/yx7xezcf%28v=vs.110%29.aspx )
Nir
11

W nowej AppDomain spróbuj ustawić procedurę obsługi zdarzeń AssemblyResolve . To zdarzenie jest wywoływane, gdy brakuje zależności.

David
źródło
Tak nie jest. W rzeczywistości otrzymujesz wyjątek w linii, w której rejestrujesz to wydarzenie w nowej domenie AppDomain. Musisz zarejestrować to zdarzenie w bieżącej domenie AppDomain.
user1004959
Dzieje się tak, jeśli klasa jest dziedziczona po MarshalByRefObject. Nie, jeśli klasa jest oznaczona tylko atrybutem [Serializable].
user2126375
5

Musisz obsłużyć zdarzenia AppDomain.AssemblyResolve lub AppDomain.ReflectionOnlyAssemblyResolve (w zależności od tego, które ładowanie wykonujesz) w przypadku, gdy zestaw, do którego się odwołujesz, nie znajduje się w GAC lub na ścieżce sondowania CLR.

AppDomain.AssemblyResolve

AppDomain.ReflectionOnlyAssemblyResolve

Dustin Campbell
źródło
Więc muszę ręcznie wskazać żądany montaż? Nawet to jest w AppBase nowej domeny AppDomain? Czy jest sposób, aby tego nie robić?
abatishchev
5

Zrozumienie odpowiedzi @ user1996230 zajęło mi trochę czasu, więc zdecydowałem się podać bardziej wyraźny przykład. W poniższym przykładzie tworzę proxy dla obiektu załadowanego do innej domeny AppDomain i wywołuję metodę na tym obiekcie z innej domeny.

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"});
}
grouma
źródło
Trochę literówek w kodzie i muszę przyznać, że nie wierzyłem, że to zadziała, ale to uratowało mi życie. Dzięki za tonę.
Owen Ivory
4

Klucz jest zdarzeniem AssemblyResolve zgłoszonym przez 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"}));
}
Leslie Marshall
źródło
0

Musiałem to zrobić kilka razy i zbadałem wiele różnych rozwiązań.

Rozwiązanie, które uważam za najbardziej eleganckie i łatwe do wykonania, można zaimplementować jako takie.

1. Utwórz projekt, w którym możesz stworzyć prosty interfejs

interfejs będzie zawierał podpisy wszystkich członków, do których chcesz zadzwonić.

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

Ważne jest, aby projekt był czysty i lekki. Jest to projekt, do którego oba AppDomainmogą się odwoływać i który pozwoli nam nie odwoływać się do tego, Assemblyktóry chcemy załadować w oddzielnej domenie z naszego zestawu klienta.

2. Teraz utwórz projekt zawierający kod, który chcesz załadować oddzielnie AppDomain.

Ten projekt, podobnie jak w przypadku projektu klienta, będzie odnosił się do projektu proxy i zaimplementujesz interfejs.

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

3. Następnie w projekcie klienta załaduj kod do innego AppDomain.

Więc teraz tworzymy nowy AppDomain. Może określić lokalizację podstawową dla odniesień do zestawów. Sondowanie sprawdzi zależne zestawy w GAC oraz w bieżącym katalogu i AppDomainbazowym locie.

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

jeśli zajdzie taka potrzeba, istnieje mnóstwo różnych sposobów ładowania zespołu. Z tym rozwiązaniem możesz skorzystać w inny sposób. Jeśli masz nazwę kwalifikowaną zestawu, lubię używać, CreateInstanceAndUnwrapponieważ ładuje bajty zestawu, a następnie tworzy dla Ciebie wystąpienie typu i zwraca object, że możesz po prostu rzutować na typ serwera proxy lub jeśli nie, to w silnie wpisanym kodzie, możesz użyj środowiska uruchomieniowego języka dynamicznego i przypisz zwrócony obiekt do dynamiczmiennej o typie strukturalnym, a następnie bezpośrednio wywołaj członków.

Masz to.

Pozwala to na załadowanie zestawu, do którego Twój projekt klienta nie ma odniesienia, w osobnym AppDomaini wywołanie członków z klienta.

Aby przetestować, lubię używać okna modułów w programie Visual Studio. Pokaże Ci domenę zestawu klienta i jakie wszystkie moduły są ładowane w tej domenie, a także nową domenę aplikacji i jakie zestawy lub moduły są ładowane w tej domenie.

Kluczem jest upewnienie się, że kod pochodzi MarshalByRefObjectlub można go serializować.

`MarshalByRefObject pozwoli ci skonfigurować czas życia domeny, w której się znajduje. Przykład, powiedzmy, że chcesz, aby domena została zniszczona, jeśli serwer proxy nie został wywołany w ciągu 20 minut.

Mam nadzieję, że to pomoże.

SimperT
źródło
Cześć, jeśli dobrze pamiętam, podstawowym problemem było to, jak rekurencyjnie ładować wszystkie zależności, stąd pytanie. Przetestuj swój kod, zmieniając HelloWorld tak, aby zwracał klasę typu, Foo, FooAssemblyktóra ma właściwość type Bar, BarAssembly, czyli łącznie 3 zestawy. Czy nadal będzie działać?
abatishchev
Tak, potrzebny jest odpowiedni katalog wyliczony na etapie sondowania montażu. AppDomain ma ApplicationBase, jednak nie testowałem go. Również pliki konfiguracyjne można określić katalogi sondowania zestawu, takie jak plik app.config, z którego może korzystać biblioteka dll, a także po prostu ustawić kopiowanie we właściwościach. Ponadto, jeśli masz kontrolę nad budowaniem zestawu, który ma zostać załadowany w oddzielnej domenie aplikacji, odwołania mogą uzyskać HintPath, które określają, że mają go szukać. Gdyby to wszystko zawiodło, zasubskrybowałbym nowe zdarzenie AppDomains AssemblyResolve i ręcznie załadowałbym zestawy. Mnóstwo przykładów na to.
SimperT