2010-06-11 7 views
28

MISE À JOUR: J'ai maintenant une solution Je suis beaucoup plus heureux avec cela, tout en ne résolvant pas tous les problèmes que je pose, il laisse le champ libre pour le faire . J'ai mis à jour ma propre réponse pour refléter cela.Comment pré-charger tous les assemblys déployés pour un AppDomain

Original Question

Compte tenu d'un domaine App, il y a beaucoup d'endroits différents que la fusion (le chargeur d'assemblage .Net) sondera pour un ensemble donné. De toute évidence, nous prenons cette fonctionnalité pour acquis et, puisque le sondage semble être intégré dans l'exécution. Net (Assembly._nLoad méthode interne semble être le point d'entrée lorsque Reflect-Loading - et je suppose que le chargement implicite est probablement couvert par le même algorithme sous-jacent), en tant que développeurs, nous ne semblons pas pouvoir accéder à ces chemins de recherche. Mon problème est que j'ai un composant qui fait beaucoup de résolution de type dynamique, et qui doit pouvoir s'assurer que tous les assemblys déployés par l'utilisateur pour un AppDomain donné sont préchargés avant qu'il ne commence son travail. Oui, il ralentit le démarrage - mais les avantages que nous obtenons de ce composant surpoids tout cela.

L'algorithme de chargement de base que j'ai déjà écrit est le suivant. Il scanne en profondeur un ensemble de dossiers pour tout fichier .dll (.exes sont exclus en ce moment), et utilise Assembly.LoadFrom pour charger la DLL si son AssemblyName est introuvable dans l'ensemble des assemblys déjà chargés dans AppDomain (ce qui est mis en œuvre inefficacement, mais il peut être optimisé plus tard):

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 est utilisé parce que je l'ai trouvé que l'utilisation Load() peut conduire à dupliquer des ensembles chargés par fusion si, quand il sonde pour elle , il n'en trouve pas un chargé d'où il s'attend à le trouver. Donc, avec ceci en place, tout ce que j'ai maintenant à faire est d'obtenir une liste dans l'ordre de priorité (du plus haut au plus bas) des chemins de recherche que Fusion va utiliser quand il cherche un assembly. Ensuite, je peux simplement itérer à travers eux. Le GAC est sans importance pour cela, et je ne suis pas intéressé par les chemins fixes basés sur l'environnement que Fusion pourrait utiliser - seulement les chemins qui peuvent être glanés à partir de l'AppDomain qui contiennent des assemblages expressément déployés pour l'application.

Ma première itération de ce AppDomain.BaseDirectory simplement utilisé. Cela fonctionne pour les services, les applications de formulaire et les applications de console. Toutefois, cela ne fonctionne pas pour un site Web Asp.Net, car il existe au moins deux emplacements principaux: AppDomain.DynamicDirectory (où Asp.Net place ses classes de pages générées dynamiquement et tous les assemblys que le code de page Aspx références), puis le dossier Bin du site - qui peut être découvert à partir de la propriété AppDomain.SetupInformation.PrivateBinPath. J'ai maintenant du code de travail pour les types d'applications les plus basiques (les AppDomains hébergés par Sql Server sont une autre histoire depuis que le système de fichiers est virtualisé) - mais j'ai rencontré un problème intéressant il y a quelques jours où ce code ne fonctionne pas: le coureur de test nUnit. Ceci utilise à la fois la copie fantôme (donc mon algorithme aurait besoin de les découvrir et de les charger depuis le dossier de dépôt de copie fantôme, pas du dossier bin) et il définit le PrivateBinPath comme étant relatif au répertoire de base.

Et bien sûr, il y a plein d'autres scénarios d'hébergement que je n'ai probablement pas envisagés; mais qui doit être valide, sinon Fusion fusionnerait lors du chargement des assemblages. Je veux arrêter de me sentir à l'aise et introduire le piratage pour accommoder ces nouveaux scénarios à mesure qu'ils surgissent - ce que je veux, étant donné un AppDomain et ses informations d'installation, la capacité de produire cette liste de dossiers que je devrais scanner afin de ramasser toutes les DLL qui vont être chargées; indépendamment de la façon dont le AppDomain est configuré. Si Fusion peut les voir tout de même, alors mon code devrait le devenir.

Bien sûr, je devrais peut-être modifier l'algorithme si .Net change ses internes - c'est juste une croix que je devrai supporter. De même, je suis heureux de considérer SQL Server et tous les autres environnements similaires comme des cas limites qui ne sont pas pris en charge pour l'instant.

Des idées!?

+0

pas vraiment une réponse, mais très utile pour moi était ce MSDN-article: http://msdn.microsoft.com/en-us/library/yx7xezcf. aspx). Et le Fuslogvw.exe mentionné devrait aider à obtenir le "pourquoi est-ce que dll pas trouvé". –

+0

ralf.w: Je me suis demandé si je pouvais tricher en obtenant un journal de fusion pour un assemblage inexistant et l'analyser pour voir tous les endroits qu'il recherche. Bien que cela fonctionne probablement, j'ai l'impression que je devrais activer Fusion Logging sur la case cible (= lent); plus le fait que je coderais par exception, ce qui est tout simplement faux! Merci pour le lien vers l'article - peut-être un peu plus MSDN minage pourrait me donner la réponse ... –

+0

J'ai exactement ce problème, seriez-vous prêt à partager le code que vous êtes arrivé? merci :) –

Répondre

18

J'ai maintenant été en mesure d'obtenir quelque chose de plus proche d'une solution finale, sauf qu'elle ne traite pas correctement le chemin de la corbeille privée. J'ai remplacé mon code live précédent par ceci et ai également résolu quelques bogues méchants d'exécution que j'ai eu dans l'affaire (compilation dynamique du code C# référençant beaucoup trop de dlls).

La règle d'or que j'ai depuis découvert est always use the load context, pas le contexte LoadFrom, puisque le contexte de chargement sera toujours le premier endroit .Net apparaît lors de l'exécution d'une liaison naturelle. Par conséquent, si vous utilisez le contexte LoadFrom, vous n'obtiendrez un hit que si vous le chargez réellement depuis le même endroit auquel il le lierait naturellement, ce qui n'est pas toujours facile.

Cette solution fonctionne à la fois pour les applications Web, en tenant compte de la différence de dossier bin par rapport aux applications «standard». Il peut facilement être étendu pour accueillir le problème PrivateBinPath, une fois que je peux obtenir une poignée fiable sur exactement comment il est lu (!)

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

D'abord, nous avons la méthode utilisée pour récupérer nos choisis "dossiers d'applications. Ce sont les endroits où les assemblages déployés par l'utilisateur auront été déployés. Il est un IEnumerable à cause de l'affaire de bord PrivateBinPath (il peut être une série d'emplacements), mais dans la pratique, il est seulement jamais un dossier au moment:

La méthode suivante est PreLoadDeployedAssemblies(), qui est appelé avant de faire quoi que ce soit (ici, il est listé dans private static - dans mon code ceci est pris d'une classe statique beaucoup plus grande qui a des extrémités publiques qui vont toujours déclencher ce code pour être exécuté avant de faire quoi que ce soit pour la première fois

Enfin, il y a la viande et les os. chose importante ici est de prendre un fichier d'assemblage et obtenir son nom de l'assemblage, que vous passez ensuite à Assembly.Load(AssemblyName) - et de ne pas utiliser LoadFrom.

Je pensais précédemment que LoadFrom était plus fiable, et que vous deviez aller manuellement et trouver le dossier temporaire Asp.Net dans les applications web. Vous ne le faites pas. Tout ce que vous avez à faire est de connaître le nom d'un assemblage dont vous savez qu'il devrait être chargé - et de le passer à Assembly.Load. Après tout, c'est pratiquement ça.Les routines de chargement de référence de Net font :)

De même, cette approche fonctionne bien avec le sondage d'assemblage personnalisé mis en œuvre par la suspension de l'événement AppDomain.AssemblyResolve: Étendez les dossiers bin de l'application à tous les dossiers de conteneurs de plugins afin qu'ils soient analysés. Il est probable que vous ayez déjà traité l'événement AssemblyResolve pour vous assurer qu'ils sont chargés lorsque le sondage normal échoue, de sorte que tout fonctionne comme avant.

+0

C'est génial. Exactement ce que je cherchais. Merci d'avoir posté du code mis à jour !!!! –

+0

faites-vous une vérification du fichier dll avant vous Assembly.Load pour vérifier si c'est même un. NET dll, ou attrapez-vous simplement l'exception et passer à autre chose? – JJS

+0

@JJS - ouais tous les points de rupture potentiels seraient dans un 'try/catch' - dans la situation où je l'ai utilisé, nous n'avions pas de DLL non-CLR, donc ce n'était jamais un problème, mais il devrait être bien. –

0

Avez-vous essayé en regardant Assembly.GetExecutingAssembly(). Cela devrait vous donner le chemin vers l'assembly où votre code est exécuté. Dans le cas NUnit, je m'attendrais à ce que ce soit là où les assemblées ont été copiées.

+0

Ouais, c'était l'une de mes premières incursions dans ce domaine, mais ce n'est pas fiable dans certains des plus améliorés situations (le coureur de test VS, par exemple). En outre, dans le cas d'Asp.Net, par exemple, Assembly Loader possède en réalité de nombreux dossiers qu'il recherchera en dehors de l'emplacement de l'assembly d'exécution. –

+0

Je suis tout à fait d'accord pour dire que ce n'est pas suffisant en soi pour couvrir tous les cas ... Je pensais que cela vous donnerait simplement un point de plus. Donc, si je vous comprends bien, cherchez-vous une API que vous pouvez appeler qui vous donnera tous les chemins de recherche Fusions? – Andy

+0

bien ce serait le Saint Graal à coup sûr; Je ne vois rien dans le framework .Net qui le fait et comme pour les API non gérées; Je n'ai pas peur de ça (je me suis fait des dents en C++ pendant de nombreuses années), mais il est difficile de trouver quoi que ce soit. J'ai regardé l'API Fusion, mais c'est principalement pour travailler avec le GAC. –

4

C'est ce que je fais:

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

intéressant - cela ressemble à une bonne solution; cependant, le dossier PrivateBinPath peut être, apparemment, une liste de dossiers séparés par des points-virgules sous ApplicationBase à partir desquels les DLL seront chargées.Ils peuvent être relatifs ou absolus (bien que ceux-ci seront ignorés s'ils ne sont pas sous ApplicationBase). Alors peut-être qu'un tweak est nécessaire là-bas. En outre - dans une application Asp.Net, il est important de charger les dll en premier dans le répertoire temporaire Asp.Net lors de l'utilisation de LoadFrom. Si vous chargez A.dll à partir de bin, le runtime le chargera à nouveau à partir du dossier temporaire et vous obtiendrez deux copies du même assemblage :( –

+0

im l'utiliser dans une application asp.net et n'a pas eu de problèmes jusqu'à présent J'ai besoin de charger à partir de la température d'abord? Grrr c'est un tel problème à avoir –

+0

oui vous devez être absolument sûr (il semble dépendre de l'endroit où l'exécution localisera l'assemblage si chargé organiquement, et j'ai seulement réussi à supprimer les dupes de cette façon) .Ce fut une douleur dans le cul de déboguer ce problème aussi! Cependant - +1 de toute façon :) –

Questions connexes