Come caricare un assembly in AppDomain con tutti i riferimenti in modo ricorsivo?


113

Voglio caricare un nuovo AppDomainassieme che ha un complesso albero di riferimenti (MyDll.dll -> Microsoft.Office.Interop.Excel.dll -> Microsoft.Vbe.Interop.dll -> Office.dll -> stdole.dll)

Per quanto ho capito, quando viene caricato un assembly AppDomain, i suoi riferimenti non vengono caricati automaticamente e devo caricarli manualmente. Quindi quando lo faccio:

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

e ottenuto FileNotFoundException:

Impossibile caricare il file o l'assembly "MyDll, Version = 1.0.0.0, Culture = neutral, PublicKeyToken = null" o una delle sue dipendenze. Il sistema non trova il file specificato.

Penso che la parte fondamentale sia una delle sue dipendenze .

Ok, lo faccio prima domain.Load(AssemblyName.GetAssemblyName(path));

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

Ma ottenuto di FileNotFoundExceptionnuovo, su un altro assembly (referenziato).

Come caricare ricorsivamente tutti i riferimenti?

Devo creare un albero dei riferimenti prima di caricare l'assembly root? Come ottenere i riferimenti di un assembly senza caricarlo?


1
Ho caricato assembly in questo modo molte volte in passato, non ho mai dovuto caricare manualmente tutti i suoi riferimenti. Non sono sicuro che la premessa di questa domanda sia corretta.
Mick

Risposte:


68

È necessario invocare CreateInstanceAndUnwrapprima che l'oggetto proxy venga eseguito nel dominio dell'applicazione esterna.

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

Inoltre, tieni presente che se usi LoadFromprobabilmente otterrai FileNotFoundun'eccezione perché il risolutore di assembly tenterà di trovare l'assembly che stai caricando nella GAC ​​o nella cartella bin dell'applicazione corrente. Utilizzare invece LoadFileper caricare un file assembly arbitrario, ma si noti che se si esegue questa operazione sarà necessario caricare personalmente le dipendenze.


20
Controlla il codice che ho scritto per risolvere questo problema: github.com/jduv/AppDomainToolkit . In particolare, guarda il metodo LoadAssemblyWithReferences in questa classe: github.com/jduv/AppDomainToolkit/blob/master/AppDomainToolkit/…
Jduv

3
Ho scoperto che sebbene questo funzioni per la maggior parte del tempo, in alcuni casi è ancora necessario allegare un gestore AppDomain.CurrentDomain.AssemblyResolveall'evento come descritto in questa risposta MSDN . Nel mio caso, stavo cercando di collegarmi alla distribuzione SpecRun in esecuzione sotto MSTest, ma penso che si applichi a molte situazioni in cui il tuo codice potrebbe non essere eseguito
dall'AppDomain

Ah interessante. Lo esaminerò e vedrò se riesco a renderlo leggermente più facile da lavorare tramite ADT. Mi dispiace che il codice sia leggermente morto da un po 'di tempo - abbiamo tutti lavori giornalieri :).
Jduv

@ Jduv Voterei il tuo commento circa 100 volte se potessi. La tua libreria mi ha aiutato a risolvere un problema apparentemente irrisolvibile che stavo avendo con il caricamento di assembly dinamico in MSBuild. Dovresti promuoverlo a una risposta!
Philip Daniels

2
@Jduv sei sicuro che la assemblyvariabile farà riferimento all'assembly da "MyDomain"? Penso var assembly = value.GetAssembly(args[0]);che caricherai il tuo args[0]in entrambi i domini e la assemblyvariabile farà riferimento alla copia dal dominio dell'applicazione principale
Igor Bendrup

14

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

Versione C #:

Crea una classe moderatore ed ereditala da MarshalByRefObject:

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

chiamata dal sito del cliente

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

6
Qualcuno può spiegare come viene inserita questa soluzione nel contesto della creazione di un nuovo AppDomain?
Tri Q Tran

2
A MarshalByRefObjectpuò essere passato agli appdomain. Quindi immagino che Assembly.LoadFromcerchi di caricare l'assembly in un nuovo appdomain, cosa possibile solo se l'oggetto chiamante potesse essere passato tra quegli appdomain. Questo è anche chiamato remoting come descritto qui: msdn.microsoft.com/en-us/library/…
Christoph Meißner,

32
Questo non funziona. Se esegui il codice e controlli AppDomain.CurrentDomain.GetAssemblies () vedrai che l'assembly di destinazione che stai tentando di caricare è caricato nel dominio dell'applicazione corrente e non in quello proxy.
Jduv

41
Questa è una totale assurdità. Ereditare da MarshalByRefObjectnon lo fa magicamente caricare in ogni altro AppDomain, dice semplicemente al framework .NET di creare un proxy remoto trasparente invece di usare la serializzazione quando si scarta il riferimento da uno AppDomainin un altro AppDomain(il modo tipico è il CreateInstanceAndUnwrapmetodo). Non posso credere che questa risposta abbia più di 30 voti positivi; il codice qui è solo un modo inutile di chiamare Assembly.LoadFrom.
Aaronaught

1
Sì, sembra una totale assurdità, eppure ha 28 voti positivi ed è contrassegnata come risposta. Il collegamento fornito non menziona nemmeno MarshalByRefObject. Abbastanza bizzarro. Se questo fa davvero qualcosa, mi piacerebbe che qualcuno spiegasse come
Mick

12

Dopo aver passato l'istanza dell'assembly al dominio del chiamante, il dominio del chiamante proverà a caricarlo! Questo è il motivo per cui ottieni l'eccezione. Questo accade nell'ultima riga di codice:

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

Quindi, qualunque cosa tu voglia fare con l'assembly, dovrebbe essere fatto in una classe proxy, una classe che eredita MarshalByRefObject .

Tieni presente che il dominio del chiamante e il nuovo dominio creato dovrebbero avere entrambi accesso all'assembly della classe proxy. Se il tuo problema non è troppo complicato, considera di lasciare la cartella ApplicationBase invariata, quindi sarà uguale alla cartella del dominio del chiamante (il nuovo dominio caricherà solo gli assembly di cui ha bisogno).

In codice semplice:

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

Se è necessario caricare gli assembly da una cartella diversa dalla cartella del dominio dell'app corrente, creare il nuovo dominio dell'app con una cartella del percorso di ricerca dll specifica.

Ad esempio, la riga di creazione del dominio dell'app dal codice precedente dovrebbe essere sostituita con:

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

In questo modo, tutte le DLL verranno automaticamente risolte da dllsSearchPath.


Perché devo caricare l'assembly utilizzando una classe proxy? Qual è la differenza rispetto al caricamento tramite Assembly.LoadFrom (string). Sono interessato ai dettagli tecnici, dal punto di vista del CLR. Ti sarei molto grato se potessi fornire una risposta.
Dennis Kassel

Si utilizza la classe proxy per evitare che il nuovo assembly venga caricato nel dominio del chiamante. Se si utilizza Assembly.LoadFrom (stringa), il dominio del chiamante tenterà di caricare i nuovi riferimenti di assembly e non li troverà perché non cerca gli assembly in "[AsmPath]". ( msdn.microsoft.com/en-us/library/yx7xezcf%28v=vs.110%29.aspx )
Nir

11

Nel nuovo AppDomain, prova a impostare un gestore di eventi AssemblyResolve . Quell'evento viene chiamato quando manca una dipendenza.


Non è così. In realtà, ottieni un'eccezione sulla linea in cui stai registrando questo evento sul nuovo AppDomain. Devi registrare questo evento sull'AppDomain corrente.
user1004959

Lo fa se la classe viene ereditata da MarshalByRefObject. Non lo fa se la classe è contrassegnata solo con l'attributo [Serializable].
user2126375

5

È necessario gestire gli eventi AppDomain.AssemblyResolve o AppDomain.ReflectionOnlyAssemblyResolve (a seconda del carico che si sta eseguendo) nel caso in cui l'assembly a cui si fa riferimento non si trovi nella GAC ​​o nel percorso di rilevamento del CLR.

AppDomain.AssemblyResolve

AppDomain.ReflectionOnlyAssemblyResolve


Quindi devo indicare manualmente il montaggio richiesto? Anche se è nella nuova AppBase di AppDomain? C'è un modo per non farlo?
abatishchev

5

Mi ci è voluto un po 'per capire la risposta di @ user1996230, quindi ho deciso di fornire un esempio più esplicito. Nell'esempio seguente creo un proxy per un oggetto caricato in un altro AppDomain e chiamo un metodo su quell'oggetto da un altro dominio.

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

Alcuni piccoli errori di battitura nel codice, e devo ammettere che non credevo che avrebbe funzionato, ma questo mi ha salvato la vita. Grazie mille.
Owen Ivory

4

La chiave è l'evento AssemblyResolve generato da 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

Ho dovuto farlo diverse volte e ho cercato molte soluzioni diverse.

La soluzione che trovo più elegante e di facile realizzazione può essere implementata come tale.

1. Creare un progetto che è possibile creare una semplice interfaccia

l'interfaccia conterrà le firme di tutti i membri che desideri chiamare.

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

È importante mantenere questo progetto pulito e leggero. È un progetto a cui entrambi AppDomainpossono fare riferimento e ci permetterà di non fare riferimento a quello Assemblyche desideriamo caricare in un dominio separato dal nostro assembly client.

2. Ora crea il progetto che ha il codice che vuoi caricare separatamente AppDomain.

Questo progetto come con il progetto del cliente farà riferimento al progetto proxy e implementerai l'interfaccia.

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

3. Successivamente, nel progetto client, caricare il codice in un altro AppDomain.

Quindi, ora creiamo un nuovo file AppDomain. Può specificare la posizione di base per i riferimenti di assieme. L'analisi verificherà la presenza di assembly dipendenti in GAC e nella directory corrente e nella posizione di AppDomainbase.

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

se necessario, ci sono molti modi diversi per caricare un assieme. Puoi usare un modo diverso con questa soluzione. Se hai il nome qualificato dell'assembly, mi piace usare CreateInstanceAndUnwrappoiché carica i byte dell'assembly e quindi istanzia il tuo tipo per te e restituisce un objectche puoi semplicemente trasmettere al tuo tipo di proxy o se non lo fai in codice fortemente digitato potresti utilizzare il runtime del linguaggio dinamico e assegnare l'oggetto restituito a una dynamicvariabile digitata, quindi chiamare direttamente i membri su quella.

Ecco qua.

Ciò consente di caricare un assembly a cui il progetto client non ha riferimento in un separato AppDomaine chiamare membri su di esso dal client.

Per testare, mi piace usare la finestra Moduli in Visual Studio. Ti mostrerà il dominio dell'assembly client e quali sono tutti i moduli caricati in quel dominio, nonché il tuo nuovo dominio dell'app e quali assembly o moduli vengono caricati in quel dominio.

La chiave è assicurarsi che il codice derivi MarshalByRefObjecto sia serializzabile.

`MarshalByRefObject ti permetterà di configurare la durata del dominio in cui si trova. Esempio, supponi di voler distruggere il dominio se il proxy non è stato chiamato in 20 minuti.

Spero che aiuti.


Salve, se ricordo bene, il problema principale era come caricare tutte le dipendenze in modo ricorsivo, da cui la domanda. Si prega di testare il codice modificando HelloWorld per restituire una classe di tipo Foo, FooAssemblyche ha una proprietà di tipo Bar, BarAssembly, cioè 3 assembly in totale. Continuerebbe a funzionare?
abatishchev

Sì, è necessaria la directory corretta enumerata nella fase di verifica dell'assembly. AppDomain ha un ApplicationBase, tuttavia, non l'ho testato. Anche i file di configurazione è possibile specificare directory di rilevamento dell'assembly come app.config che può essere utilizzato anche da una dll impostata per la copia nelle proprietà. Inoltre, se hai il controllo sulla creazione dell'assembly che desidera caricare in un dominio app separato, i riferimenti possono ottenere un HintPath che specifica dove cercarlo. Se tutto ciò fallisse, mi iscriverei al nuovo evento AppDomains AssemblyResolve e caricare manualmente gli assembly.
SimperT
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.