2010-06-11 11 views
28

UPDATE: Ora ho una soluzione Sono molto più contento di questo, pur non risolvendo tutti i problemi che chiedo, lascia la strada libera per farlo . Ho aggiornato la mia risposta per riflettere questo.Come precaricare tutti gli assembly distribuiti per un AppDomain

domanda iniziale

Dato un dominio App, ci sono molti luoghi diversi che la fusione (l'assemblea loader .Net) controllerà la data di assemblaggio. Ovviamente, diamo per scontata questa funzionalità e, dal momento che il sondaggio sembra essere incorporato nel .Net runtime (Assembly._nLoad, il metodo interno sembra essere il punto di ingresso quando Reflect-Loading - e presumo che il caricamento implicito sia probabilmente coperto dallo stesso algoritmo di base), in quanto sviluppatori non sembra essere in grado di accedere a quei percorsi di ricerca.

Il mio problema è che ho un componente che esegue molta risoluzione di tipo dinamico e che deve essere in grado di assicurare che tutti gli assembly distribuiti dall'utente per un determinato AppDomain siano precaricati prima di iniziare il lavoro. Sì, rallenta l'avvio - ma i benefici che otteniamo da questo componente sono totalmente in eccesso.

L'algoritmo di caricamento di base che ho già scritto è il seguente. Esamina in profondità una serie di cartelle per qualsiasi .dll (.exes vengono esclusi al momento) e utilizza Assembly.LoadFrom per caricare la DLL se non è possibile trovare AssemblyName nel set di assiemi già caricati nell'AppDomain (questo è implementato inefficiente, ma può essere ottimizzato in seguito):

void PreLoad(IEnumerable<string> paths) 
{ 
    foreach(path p in paths) 
    { 
    PreLoad(p); 
    } 
} 

void PreLoad(string p) 
{ 
    //all try/catch blocks are elided for brevity 
    string[] files = null; 

    files = Directory.GetFiles(p, "*.dll", SearchOption.AllDirectories); 

    AssemblyName a = null; 
    foreach (var s in files) 
    { 
    a = AssemblyName.GetAssemblyName(s); 
    if (!AppDomain.CurrentDomain.GetAssemblies().Any(
     assembly => AssemblyName.ReferenceMatchesDefinition(
     assembly.GetName(), a))) 
     Assembly.LoadFrom(s); 
    }  
} 

LoadFrom viene utilizzato perché ho trovato che utilizzando Load() può portare a duplicare assiemi corso di caricamento da Fusion se, quando esso esplora essa , non ne trova uno caricato da dove si aspetta di trovarlo.

Quindi, con questo in atto, tutto ciò che devo fare ora è ottenere un elenco in ordine di precedenza (dal più alto al più basso) dei percorsi di ricerca che Fusion sta per utilizzare quando cerca un assembly. Quindi posso semplicemente scorrere attraverso di loro.

Il GAC è irrilevante per questo, e non mi interessa alcun percorso fisso guidato dall'ambiente che Fusion potrebbe utilizzare: solo quei percorsi che possono essere ricavati dall'AppDomain che contengono gli assembly espressamente distribuiti per l'app.

La mia prima iterazione di questa semplice AppDomain.BaseDirectory. Funziona per servizi, form app e console.

Non funziona per un sito Web Asp.Net, tuttavia, dal momento che ci sono almeno due percorsi principali: l'AppDomain.DynamicDirectory (dove Asp.Net colloca le sue classi di pagine generate dinamicamente e tutti gli assembly che il codice di pagina Aspx riferimenti), quindi la cartella Bin del sito, che può essere rilevata dalla proprietà AppDomain.SetupInformation.PrivateBinPath.

Così ora ho un codice funzionante per i tipi di applicazioni più elementari ora (gli AppDomain ospitati da Sql Server sono un'altra storia da quando il filesystem è virtualizzato) - ma ho trovato un problema interessante un paio di giorni fa in cui questo codice semplicemente non funziona: il nUnit test runner.

Questo utilizza sia la copia ombreggiatura (quindi il mio algoritmo dovrebbe trovarsi e caricarli dalla cartella di rilascio della copia shadow, non dalla cartella bin) e imposta PrivateBinPath come relativo alla directory di base.

E ovviamente ci sono molti altri scenari di hosting che probabilmente non ho considerato; ma che deve essere valido perché altrimenti Fusion si strozzerebbe durante il caricamento degli assiemi.

Voglio smettere di sentirmi intorno e introdurre l'hack su hack per accogliere questi nuovi scenari mentre emergono - quello che voglio è, dato un AppDomain e le sue informazioni di installazione, la capacità di produrre questo elenco di cartelle che dovrei scansionare per raccogliere tutte le DLL che verranno caricate; indipendentemente da come è configurato l'AppDomain. Se Fusion può vederli come tutti uguali, allora dovrebbe essere il mio codice.

Naturalmente, potrei dover modificare l'algoritmo se .Net cambia i suoi interni - è solo una croce che dovrò sopportare. Allo stesso modo, sono felice di prendere in considerazione SQL Server e altri ambienti simili come edge case che al momento non sono supportati.

Qualche idea !?

+0

davvero una risposta, ma molto utile per me è stato questo MSDN-articolo: http://msdn.microsoft.com/en-us/library/yx7xezcf. aspx). E il Fuslogvw.exe menzionato dovrebbe aiutare a ottenere il "perché questa DLL non è stata trovata". –

+0

ralf.w: Mi chiedevo se potevo imbrogliare ottenendo un registro di fusione per un assemblaggio inesistente e analizzarlo per vedere tutti i posti che sta guardando. Anche se probabilmente funzionerebbe, ho la sensazione di dover attivare Fusion Logging sulla casella di destinazione (= slow); oltre al fatto che codificherei per eccezione, il che è semplicemente sbagliato! Grazie per il link all'articolo - forse un po 'di più MSDN mining potrebbe darmi la risposta ... –

+0

Ho esattamente questo problema, saresti disposto a condividere il codice che hai ricevuto? grazie :) –

risposta

18

Sono ora in grado di ottenere qualcosa di molto più vicino a una soluzione finale, tranne che non sta ancora elaborando il percorso del cestino privato correttamente. Ho sostituito il mio codice precedente con questo e ho anche risolto alcuni brutti bug di runtime che ho avuto nell'affare (compilazione dinamica del codice C# che fa riferimento a troppe dll).

La regola d'oro che ho scoperto è always use the load context, non il contesto LoadFrom, poiché il contesto di caricamento sarà sempre il primo posto .Net appare quando si esegue un binding naturale. Pertanto, se si utilizza il contesto LoadFrom, si otterrà un hit solo se lo si carica effettivamente dallo stesso punto in cui lo collegherebbe in modo naturale - il che non è sempre facile.

Questa soluzione funziona sia per le applicazioni Web, tenendo conto della differenza della cartella bin rispetto alle app "standard". Può essere facilmente esteso per ospitare il problema PrivateBinPath, una volta che posso ottenere una maniglia affidabile su esattamente come si legge (!)

private static IEnumerable<string> GetBinFolders() 
{ 
    //TODO: The AppDomain.CurrentDomain.BaseDirectory usage is not correct in 
    //some cases. Need to consider PrivateBinPath too 
    List<string> toReturn = new List<string>(); 
    //slightly dirty - needs reference to System.Web. Could always do it really 
    //nasty instead and bind the property by reflection! 
    if (HttpContext.Current != null) 
    { 
    toReturn.Add(HttpRuntime.BinDirectory); 
    } 
    else 
    { 
    //TODO: as before, this is where the PBP would be handled. 
    toReturn.Add(AppDomain.CurrentDomain.BaseDirectory); 
    } 

    return toReturn; 
} 

private static void PreLoadDeployedAssemblies() 
{ 
    foreach(var path in GetBinFolders()) 
    { 
    PreLoadAssembliesFromPath(path); 
    } 
} 

private static void PreLoadAssembliesFromPath(string p) 
{ 
    //S.O. NOTE: ELIDED - ALL EXCEPTION HANDLING FOR BREVITY 

    //get all .dll files from the specified path and load the lot 
    FileInfo[] files = null; 
    //you might not want recursion - handy for localised assemblies 
    //though especially. 
    files = new DirectoryInfo(p).GetFiles("*.dll", 
     SearchOption.AllDirectories); 

    AssemblyName a = null; 
    string s = null; 
    foreach (var fi in files) 
    { 
    s = fi.FullName; 
    //now get the name of the assembly you've found, without loading it 
    //though (assuming .Net 2+ of course). 
    a = AssemblyName.GetAssemblyName(s); 
    //sanity check - make sure we don't already have an assembly loaded 
    //that, if this assembly name was passed to the loaded, would actually 
    //be resolved as that assembly. Might be unnecessary - but makes me 
    //happy :) 
    if (!AppDomain.CurrentDomain.GetAssemblies().Any(assembly => 
     AssemblyName.ReferenceMatchesDefinition(a, assembly.GetName()))) 
    { 
     //crucial - USE THE ASSEMBLY NAME. 
     //in a web app, this assembly will automatically be bound from the 
     //Asp.Net Temporary folder from where the site actually runs. 
     Assembly.Load(a); 
    } 
    } 
} 

In primo luogo abbiamo il metodo utilizzato per recuperare le nostre scelte 'cartelle app'. Questi sono i luoghi in cui saranno stati distribuiti gli assembly distribuiti dall'utente. Si tratta di un IEnumerable a causa del caso PrivateBinPath bordo (può essere una serie di posizioni), ma in pratica è sempre e solo una cartella in questo momento:

Il metodo successivo è PreLoadDeployedAssemblies(), che viene chiamato prima di fare qualsiasi cosa (qui è elencato come private static - nel mio codice questo è preso da una classe statica molto più grande che ha endpoint pubblici che attiveranno sempre questo codice per essere eseguito prima di fare qualsiasi cosa per la prima volta

Infine c'è la carne e le ossa. cosa importante qui è prendere un file assembly e ottenere il nome assembly, che quindi passare a Assembly.Load(AssemblyName) - e non utilizzare LoadFrom.

Prima pensavo che LoadFrom fosse più affidabile e che dovevi andare manualmente a trovare la cartella temporanea di Asp.Net nelle app web. Non lo fai. Tutto quello che devi è conoscere il nome di un assembly che sai dovrebbe essere sicuramente caricato e passarlo a Assembly.Load. Dopotutto, è praticamente ciò che.Le routine di caricamento di riferimento di Net do :)

Allo stesso modo, questo approccio funziona bene con il rilevamento personalizzato dell'assemblaggio implementato dall'appendersi allo stesso tempo dell'evento AppDomain.AssemblyResolve: Estendere le cartelle bin dell'app a eventuali cartelle del contenitore di plug-in che si possono acquisire. Probabilmente hai già gestito l'evento AssemblyResolve per assicurarti che vengano caricati quando il sondaggio normale fallisce, quindi tutto funziona come prima.

+0

Questo è fantastico. Proprio quello che stavo cercando. Grazie per aver pubblicato il codice aggiornato !!!! –

+0

stai facendo qualche controllo sul file dll prima di Assembly.oad per controllare se è anche una .net dll, o ti capita di rilevare l'eccezione e andare avanti? – JJS

+0

@JJS - sì, tutti i potenziali punti di rottura sarebbero in un 'try/catch' - nella situazione in cui l'ho usato non abbiamo avuto DLL non CLR, quindi non è mai stato un problema, ma dovrebbe Stammi bene. –

0

Hai provato a guardare Assembly.GetExecutingAssembly(). Posizione? Questo dovrebbe darti il ​​percorso all'assembly da cui il tuo codice sta scappando. Nel caso NUnit, mi aspetterei che fosse il luogo in cui gli assiemi sono stati copiati in ombra.

+0

Sì, quella fu una delle mie prime incursioni nel fare questo, ma è inaffidabile in alcuni dei più avanzati situazioni (ad esempio, il correttore di test VS). Inoltre, nel caso di Asp.Net, ad esempio, il Caricatore di assiemi contiene effettivamente numerose cartelle che cercherà all'esterno della posizione dell'assieme di esecuzione. –

+0

Sono assolutamente d'accordo che non è sufficiente da solo per coprire tutti i casi ... Stavo pensando che ti avrebbe dato semplicemente un altro punto dati. Quindi se ti capisco correttamente, stai cercando una API che puoi chiamare che ti fornirà tutti i percorsi di ricerca di Fusions? – Andy

+0

beh questo sarebbe sicuramente il Santo Graal; Non riesco a vedere nulla nel framework .Net che lo fa e come per le API non gestite; Non ho paura di quella roba (mi sono fatto i denti in C++ per molti anni), ma è difficile trovare qualcosa. Ho dato un'occhiata all'API Fusion, ma è principalmente per lavorare con GAC –

4

Questo è quello che faccio:

public void PreLoad() 
{ 
    this.AssembliesFromApplicationBaseDirectory(); 
} 

void AssembliesFromApplicationBaseDirectory() 
{ 
    string baseDirectory = AppDomain.CurrentDomain.BaseDirectory; 
    this.AssembliesFromPath(baseDirectory); 

    string privateBinPath = AppDomain.CurrentDomain.SetupInformation.PrivateBinPath; 
    if (Directory.Exists(privateBinPath)) 
     this.AssembliesFromPath(privateBinPath); 
} 

void AssembliesFromPath(string path) 
{ 
    var assemblyFiles = Directory.GetFiles(path) 
     .Where(file => Path.GetExtension(file).Equals(".dll", StringComparison.OrdinalIgnoreCase)); 

    foreach (var assemblyFile in assemblyFiles) 
    { 
     // TODO: check it isnt already loaded in the app domain 
     Assembly.LoadFrom(assemblyFile); 
    } 
} 
non
+0

interessante: sembra una buona soluzione; tuttavia, la cartella PrivateBinPath può essere, apparentemente, un elenco di cartelle separate da punto e virgola in ApplicationBase da cui verranno caricate le DLL.Potrebbero essere relativi o assoluti (sebbene questi vengano ignorati se non sotto ApplicationBase). Quindi forse c'è bisogno di un ritocco. Inoltre, in un'app Asp.Net è importante caricare prima le DLL dalla directory temp Asp.Net quando si utilizza LoadFrom. Se caricate A.dll da bin \ il runtime lo caricherà nuovamente dalla cartella temporanea e vi ritroverete con due copie dello stesso assembly :( –

+0

im che lo utilizza in un'app di asp.net e non ha avuto problemi fino ad ora Ho bisogno di caricare dal temp prima? grrr questo è un problema così schifoso da avere. –

+0

sì si fa per essere assolutamente sicuro (sembra dipendere da dove il runtime localizzerà l'assembly se caricato in modo organico, e sono riuscito solo a rimuove gli assembly ingannati in questo modo.) È stato un rompicoglioni risolvere anche quel particolare problema! Tuttavia - +1 comunque :) –

Problemi correlati