Wie lade ich eine Assembly mit allen Referenzen rekursiv in AppDomain?

113

Ich möchte eine AppDomainAssembly mit einem komplexen Referenzbaum in eine neue Assembly laden (MyDll.dll -> Microsoft.Office.Interop.Excel.dll -> Microsoft.Vbe.Interop.dll -> Office.dll -> stdole.dll).

Soweit ich verstanden habe, werden beim Laden einer Assembly AppDomainihre Referenzen nicht automatisch geladen, und ich muss sie manuell laden. Also, wenn ich es tue:

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

und bekam FileNotFoundException:

Datei oder Assembly 'MyDll, Version = 1.0.0.0, Culture = neutral, PublicKeyToken = null' oder eine ihrer Abhängigkeiten konnte nicht geladen werden. Die angegebene Datei wurde vom System nicht gefunden.

Ich denke, der Schlüsselteil ist eine seiner Abhängigkeiten .

Ok, das mache ich als nächstes vorher domain.Load(AssemblyName.GetAssemblyName(path));

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

Aber FileNotFoundExceptionwieder auf eine andere (referenzierte) Versammlung.

Wie lade ich alle Referenzen rekursiv?

Muss ich einen Referenzbaum erstellen, bevor ich die Root-Assembly lade? Wie erhalte ich die Referenzen einer Baugruppe, ohne sie zu laden?

abatishchev
quelle
1
Ich habe Baugruppen wie diese schon oft geladen. Ich musste nie alle Referenzen manuell laden. Ich bin nicht sicher, ob die Prämisse dieser Frage richtig ist.
Mick

Antworten:

68

Sie müssen aufrufen, CreateInstanceAndUnwrapbevor Ihr Proxy-Objekt in der fremden Anwendungsdomäne ausgeführt wird.

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

Beachten Sie außerdem, dass bei Verwendung LoadFromwahrscheinlich eine FileNotFoundAusnahme auftritt, da der Assembly-Resolver versucht, die zu ladende Assembly im GAC oder im bin-Ordner der aktuellen Anwendung zu finden. Verwenden Sie LoadFilestattdessen, um eine beliebige Assembly-Datei zu laden. Beachten Sie jedoch, dass Sie in diesem Fall alle Abhängigkeiten selbst laden müssen.

Jduv
quelle
20
Schauen Sie sich den Code an, den ich geschrieben habe, um dieses Problem zu lösen: github.com/jduv/AppDomainToolkit . Schauen Sie sich insbesondere die LoadAssemblyWithReferences-Methode in dieser Klasse an: github.com/jduv/AppDomainToolkit/blob/master/AppDomainToolkit/…
Jduv
3
Ich habe festgestellt, dass dies, obwohl dies die meiste Zeit funktioniert , in einigen Fällen tatsächlich noch einen Handler an das AppDomain.CurrentDomain.AssemblyResolveEreignis anhängen muss, wie in dieser MSDN-Antwort beschrieben . In meinem Fall habe ich versucht, mich in die unter MSTest ausgeführte SpecRun-Bereitstellung einzubinden, aber ich denke, dies gilt für viele Situationen, in denen Ihr Code möglicherweise nicht über die "primäre" AppDomain ausgeführt wird - VS-Erweiterungen, MSTest usw.
Aaronaught
Ah interessant. Ich werde das untersuchen und sehen, ob ich es etwas einfacher machen kann, mit ADT zu arbeiten. Tut mir leid, dass der Code schon eine Weile tot ist - wir haben alle Tagesjobs :).
Jduv
@Jduv Würde deinen Kommentar ungefähr 100 Mal positiv bewerten, wenn ich könnte. Ihre Bibliothek hat mir geholfen, ein scheinbar unlösbares Problem zu lösen, das ich beim Laden dynamischer Assemblys unter MSBuild hatte. Sie sollten es zu einer Antwort fördern!
Philip Daniels
2
@Jduv Sind Sie sicher, dass die assemblyVariable auf die Assembly von "MyDomain" verweist? Ich denke, var assembly = value.GetAssembly(args[0]);Sie werden Ihre args[0]in beide Domänen laden und assemblyVariable wird Kopie von der Hauptanwendungsdomäne
referenzieren
14

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

C # -Version:

Erstellen Sie eine Moderatorklasse und erben Sie sie von MarshalByRefObject:

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

Anruf vom Kundenstandort

ProxyDomain pd = new ProxyDomain();
Assembly assembly = pd.GetAssembly(assemblyFilePath);
Rockvista
quelle
6
Wie wird diese Lösung in den Kontext der Erstellung einer neuen AppDomain gestellt, kann jemand erklären?
Tri Q Tran
2
A MarshalByRefObjectkann um Appdomains herumgereicht werden. Ich würde also vermuten, dass Assembly.LoadFromversucht wird, die Assembly in eine neue Appdomain zu laden, was nur möglich ist, wenn das aufrufende Objekt zwischen diesen Appdomains übergeben werden könnte. Dies wird auch als Remoting bezeichnet, wie hier beschrieben: msdn.microsoft.com/en-us/library/…
Christoph Meißner
32
Das funktioniert nicht. Wenn Sie den Code ausführen und AppDomain.CurrentDomain.GetAssemblies () überprüfen, sehen Sie, dass die Zielassembly, die Sie laden möchten, in die aktuelle Anwendungsdomäne und nicht in die Proxy- Assembly geladen wird .
Jduv
41
Das ist völliger Unsinn. Durch das Erben von wird MarshalByRefObjectes nicht auf magische Weise in jedes andere geladen. AppDomainEs weist das .NET Framework lediglich an, einen transparenten Remoting-Proxy zu erstellen, anstatt die Serialisierung zu verwenden, wenn Sie die Referenz AppDomainvoneinander entfernen AppDomain(die typische Methode ist die CreateInstanceAndUnwrapMethode). Ich kann nicht glauben, dass diese Antwort über 30 positive Stimmen hat. Der Code hier ist nur ein sinnloser Umweg Assembly.LoadFrom.
Aaronaught
1
Ja, es sieht nach völligem Unsinn aus, hat aber 28 Stimmen und ist als Antwort markiert. Der angegebene Link erwähnt nicht einmal MarshalByRefObject. Ziemlich bizarr. Wenn dies tatsächlich etwas bewirkt, würde ich gerne jemanden erklären, wie
Mick
12

Sobald Sie die Assembly-Instanz an die Anruferdomäne zurückgeben, versucht die Anruferdomäne, sie zu laden! Aus diesem Grund erhalten Sie die Ausnahme. Dies geschieht in Ihrer letzten Codezeile:

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

Was auch immer Sie mit der Assembly tun möchten, sollte in einer Proxy-Klasse ausgeführt werden - einer Klasse, die MarshalByRefObject erbt .

Berücksichtigen Sie, dass sowohl die Anruferdomäne als auch die neu erstellte Domäne Zugriff auf die Proxyklassenassembly haben sollten. Wenn Ihr Problem nicht zu kompliziert ist, sollten Sie den ApplicationBase-Ordner unverändert lassen, damit er mit dem Ordner der Anruferdomäne identisch ist (die neue Domäne lädt nur die benötigten Assemblys).

In einfachem Code:

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

Wenn Sie die Assemblys aus einem Ordner laden müssen, der sich von Ihrem aktuellen App-Domänenordner unterscheidet, erstellen Sie die neue App-Domäne mit einem bestimmten DLL-Suchpfadordner.

Beispielsweise sollte die Erstellungszeile für die App-Domäne aus dem obigen Code ersetzt werden durch:

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

Auf diese Weise werden alle DLLs automatisch aus dllsSearchPath aufgelöst.

Nir
quelle
Warum muss ich die Assembly mithilfe einer Proxy-Klasse laden? Was ist der Unterschied zum Laden mit Assembly.LoadFrom (Zeichenfolge)? Ich interessiere mich für die technischen Details aus Sicht der CLR. Ich wäre Ihnen sehr dankbar, wenn Sie mir eine Antwort geben könnten.
Dennis Kassel
Sie verwenden die Proxy-Klasse, um zu verhindern, dass die neue Assembly in Ihre Anruferdomäne geladen wird. Wenn Sie Assembly.LoadFrom (Zeichenfolge) verwenden, versucht die Aufruferdomäne, die neuen Assemblyreferenzen zu laden, und findet sie nicht, da sie im "[AsmPath]" nicht nach Assemblys sucht. ( msdn.microsoft.com/en-us/library/yx7xezcf%28v=vs.110%29.aspx )
Nir
11

Versuchen Sie in Ihrer neuen AppDomain, einen AssemblyResolve- Ereignishandler festzulegen . Dieses Ereignis wird aufgerufen, wenn eine Abhängigkeit fehlt.

David
quelle
Das tut es nicht. Tatsächlich erhalten Sie eine Ausnahme in der Zeile, in der Sie dieses Ereignis in der neuen AppDomain registrieren. Sie müssen dieses Ereignis in der aktuellen AppDomain registrieren.
user1004959
Dies ist der Fall, wenn die Klasse von MarshalByRefObject geerbt wird. Dies ist nicht der Fall, wenn die Klasse nur mit dem Attribut [Serializable] markiert ist.
user2126375
5

Sie müssen die Ereignisse AppDomain.AssemblyResolve oder AppDomain.ReflectionOnlyAssemblyResolve (je nachdem, welche Last Sie ausführen) behandeln, falls sich die referenzierte Assembly nicht im GAC oder im Prüfpfad der CLR befindet.

AppDomain.AssemblyResolve

AppDomain.ReflectionOnlyAssemblyResolve

Dustin Campbell
quelle
Muss ich also die angeforderte Baugruppe manuell angeben? Auch ist es in der neuen AppDomain AppBase? Gibt es eine Möglichkeit, das nicht zu tun?
Abatishchev
5

Ich habe eine Weile gebraucht, um die Antwort von @ user1996230 zu verstehen, und habe mich daher entschlossen, ein expliziteres Beispiel zu liefern. Im folgenden Beispiel erstelle ich einen Proxy für ein Objekt, das in eine andere AppDomain geladen wurde, und rufe eine Methode für dieses Objekt aus einer anderen Domäne auf.

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
quelle
Einige kleine Tippfehler im Code, und ich muss zugeben, dass ich nicht geglaubt habe, dass es funktionieren würde, aber das war ein Lebensretter für mich. Danke vielmals.
Owen Ivory
4

Der Schlüssel ist das AssemblyResolve-Ereignis, das von der AppDomain ausgelöst wird.

[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
quelle
0

Ich musste dies mehrmals tun und habe viele verschiedene Lösungen recherchiert.

Die Lösung, die ich am elegantesten und am einfachsten zu realisierenden finde, kann als solche implementiert werden.

1. Erstellen Sie ein Projekt, mit dem Sie eine einfache Schnittstelle erstellen können

Die Schnittstelle enthält Signaturen aller Mitglieder, die Sie anrufen möchten.

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

Es ist wichtig, dieses Projekt sauber und einfach zu halten. Es ist ein Projekt, auf das beide AppDomainverweisen können und das es uns ermöglicht, nicht auf das zu verweisen, das Assemblywir in einer separaten Domäne von unserer Client-Assembly laden möchten.

2. Erstellen Sie nun ein Projekt mit dem Code, den Sie separat laden möchten AppDomain.

Dieses Projekt verweist wie das Client-Projekt auf das Proxy-Projekt und Sie implementieren die Schnittstelle.

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

3. Laden Sie als Nächstes im Client-Projekt den Code in einen anderen AppDomain.

Also, jetzt schaffen wir eine neue AppDomain. Kann den Basisort für Baugruppenreferenzen angeben. Bei der Prüfung wird nach abhängigen Assemblys im GAC und im aktuellen Verzeichnis sowie in der AppDomainBasislok gesucht.

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

Wenn nötig, gibt es unzählige Möglichkeiten, eine Baugruppe zu laden. Mit dieser Lösung können Sie einen anderen Weg wählen. Wenn Sie den Assembly-qualifizierten Namen haben, verwende ich gerne den, CreateInstanceAndUnwrapda er die Assembly-Bytes lädt und dann Ihren Typ für Sie instanziiert und einen zurückgibt object, den Sie einfach in Ihren Proxy-Typ umwandeln können, oder wenn Sie dies nicht in stark typisierten Code tun könnten Verwenden Sie die dynamische Sprachlaufzeit und weisen Sie das zurückgegebene Objekt einer dynamictypisierten Variablen zu. Rufen Sie dann einfach die Mitglieder direkt auf.

Hier hast du es.

Auf diese Weise können Sie eine Assembly laden, auf die Ihr Client-Projekt in einem separaten Verzeichnis nicht verweist, AppDomainund Mitglieder vom Client darauf aufrufen.

Zum Testen verwende ich gerne das Modulfenster in Visual Studio. Es zeigt Ihnen Ihre Client-Assembly-Domäne und was alle Module in dieser Domäne geladen sind sowie Ihre neue App-Domäne und welche Assemblys oder Module in dieser Domäne geladen sind.

Der Schlüssel besteht darin, entweder sicherzustellen, dass der Code entweder abgeleitet MarshalByRefObjectoder serialisierbar ist.

Mit MarshalByRefObject können Sie die Lebensdauer der Domäne konfigurieren, in der sie sich befindet. Beispiel: Sie möchten, dass die Domäne zerstört wird, wenn der Proxy nicht innerhalb von 20 Minuten aufgerufen wurde.

Ich hoffe das hilft.

SimperT
quelle
Hallo, wenn ich mich richtig erinnere, war das Kernproblem, wie alle Abhängigkeiten rekursiv geladen werden können, daher die Frage. Testen Sie Ihren Code, indem Sie HelloWorld so ändern, dass eine Typklasse Foo, FooAssemblymit einer Eigenschaft vom Typ zurückgegeben wird Bar, BarAssembly, dh insgesamt 3 Assemblys. Würde es weiter funktionieren?
Abatishchev
Ja, Sie müssen das richtige Verzeichnis in der Baugruppenprüfphase auflisten. AppDomain hat eine ApplicationBase, die ich jedoch nicht getestet habe. Außerdem können Sie in Konfigurationsdateien Assembly-Prüfverzeichnisse angeben, z. B. eine app.config, die eine DLL verwenden kann, um sie in Eigenschaften zu kopieren. Wenn Sie die Kontrolle über die Erstellung der Assembly haben, die in eine separate App-Domäne geladen werden soll, können Referenzen einen HintPath erhalten, der angibt, ob danach gesucht werden soll. Wenn all dies fehlschlug, würde ich das neue AppDomains AssemblyResolve-Ereignis abonnieren und die Assemblys manuell laden. Tonnenweise Beispiele dafür.
SimperT