Comment précharger tous les assemblys déployés pour un AppDomain

MISE À JOUR: J’ai maintenant une solution qui me convient beaucoup mieux, même si je ne résous pas tous les problèmes que je pose, cela nous laisse la possibilité de le faire. J’ai mis à jour ma propre réponse pour refléter cela.

Question originale

Dans un domaine d’application, Fusion (le chargeur d’assemblages .Net) détecte de nombreux emplacements pour un assemblage donné. Évidemment, nous prenons cette fonctionnalité pour acquise et, étant donné que la vérification semble être intégrée au runtime .Net (la méthode interne Assembly._nLoad semble être le point d’entrée lorsque Reflect-Loading – et je suppose que le chargement implicite est probablement couvert par même algorithme sous-jacent), en tant que développeurs, il semble que nous ne soyons pas en mesure d’accéder à ces chemins de recherche.

Mon problème est que j’ai un composant qui effectue beaucoup de résolution de type dynamic et qui doit pouvoir garantir 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, cela ralentit le démarrage – mais les avantages de ce composant sont totalement supérieurs à ceux-ci.

L’algorithme de chargement de base que j’ai déjà écrit est le suivant. Il parsing en profondeur un ensemble de dossiers pour tout fichier .dll (les fichiers .ex sont en cours d’exclusion pour le moment ) et utilise Assembly.LoadFrom pour charger la dll si son nom AssemblyName est introuvable dans l’ensemble des assemblys déjà chargés dans le AppDomain (this. implémenté de manière inefficace, mais il peut être optimisé ultérieurement):

 void PreLoad(IEnumerable paths) { foreach(path p in paths) { PreLoad(p); } } void PreLoad(ssortingng p) { //all try/catch blocks are elided for brevity ssortingng[] 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é car j’ai constaté que l’utilisation de Load () peut entraîner le chargement d’assemblages en double par Fusion si, lors de sa recherche, il n’en trouve pas un chargé à partir de l’endroit où il s’attend à le trouver.

Donc, avec cela en place, tout ce que j’ai à faire, c’est maintenant d’obtenir une liste dans l’ordre de priorité (le plus élevé au moins élevé) des chemins de recherche que Fusion utilisera lorsqu’il recherche un assemblage. Ensuite, je peux simplement les parcourir.

Le GAC n’est pas pertinent pour cela, et je ne m’intéresse pas aux chemins fixes gérés par l’environnement que Fusion pourrait utiliser, mais uniquement aux chemins qui peuvent être glanés à partir de l’AppDomain qui contiennent des assemblys expressément déployés pour l’application.

Ma première itération de ce simplement utilisé AppDomain.BaseDirectory. 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 page générées dynamicment et tous les assemblys auxquels le code de page Aspx fait référence), et puis le dossier Bin du site – qui peut être découvert à partir de la propriété AppDomain.SetupInformation.PrivateBinPath.

J’ai donc maintenant du code de travail pour les types les plus élémentaires d’applications (les applications AppDomains hébergées par Sql Server sont une autre histoire puisque le système de fichiers est virtualisé) – mais je suis tombé sur un problème intéressant il y a quelques jours où ce code ne fonctionne tout simplement pas. : le coureur de test nUnit.

Cela utilise à la fois la copie fantôme (mon algorithme doit donc être découvert et chargé à partir du dossier de repository de copie fantôme, et non du dossier bin) et configure PrivateBinPath comme étant relatif au répertoire de base.

Et bien sûr, il existe de nombreux autres scénarios d’hébergement que je n’ai probablement pas envisagés; mais qui doit être valide car sinon Fusion serait étranglé lors du chargement des assemblys.

Je ne veux plus avoir à me soucier de quoi que ce soit et à présenter des modifications pour tenir compte de ces nouveaux scénarios au fur et à mesure qu’ils se présentent: ce que je veux, c’est qu’un AppDomain et ses informations de configuration permettent de produire cette liste de dossiers que je devrais parsingr afin de choisir toutes les DLL qui vont être chargées; quelle que soit la configuration de l’AppDomain. Si Fusion peut les voir tous de la même manière, alors mon code le devrait aussi.

Bien entendu, je pourrais être amené à modifier l’algorithme si .Net modifie ses éléments internes – c’est une croix que je devrai supporter. De même, je suis heureux de considérer SQL Server et tous les environnements similaires comme des cas marginaux non pris en charge pour le moment.

Des idées!?

Je suis maintenant en mesure de faire quelque chose de beaucoup plus proche d’une solution finale, sauf que le traitement du chemin de bac privé n’est toujours pas correct. J’ai remplacé mon code précédent par ceci et j’ai également résolu quelques bugs d’exécution désagréables (compilation dynamic de code C # référençant beaucoup trop de dll).

La règle d’or que j’ai découverte depuis est toujours d’utiliser le contexte de chargement , et non le contexte LoadFrom, car le contexte de chargement sera toujours le premier endroit où .Net regardera lors de l’exécution d’une liaison naturelle. Par conséquent, si vous utilisez le contexte LoadFrom, vous n’obtiendrez un succès que si vous le chargez réellement à partir du même endroit où il serait naturellement lié, ce qui n’est pas toujours facile.

Cette solution fonctionne aussi bien pour les applications Web que pour les applications «standard». Il peut facilement être étendu pour résoudre le problème de PrivateBinPath , une fois que je peux obtenir un descriptif fiable de la façon dont il est lu (!)

 private static IEnumerable GetBinFolders() { //TODO: The AppDomain.CurrentDomain.BaseDirectory usage is not correct in //some cases. Need to consider PrivateBinPath too List toReturn = new List(); //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(ssortingng p) { //SO 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; ssortingng 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); } } } 

Nous avons d’abord la méthode utilisée pour récupérer nos «dossiers d’application» choisis. Ce sont les endroits où les assemblys déployés par l’utilisateur auront été déployés. Il s’agit d’un IEnumerable en raison du cas d’extrémité PrivateBinPath (il peut s’agir d’une série d’emplacements), mais dans la pratique, il ne s’agit que d’un seul dossier pour le moment:

La méthode suivante est PreLoadDeployedAssemblies() , qui est appelée avant de faire quoi que ce soit (ici, elle est classée comme private static – dans mon code, elle est extraite d’une classe statique beaucoup plus grande comportant des points de terminaison publics qui déclenchent toujours l’exécution de ce code avant toute action. pour la première fois.

Enfin, il y a la viande et les os. La chose la plus importante ici est de prendre un fichier d’assembly et d’ obtenir son nom d’assembly , que vous transmettez ensuite à Assembly.Load(AssemblyName) – et de ne pas utiliser LoadFrom .

Auparavant, je pensais que LoadFrom était plus fiable et qu’il fallait rechercher manuellement le dossier temporaire Asp.Net dans les applications Web. Vous pas. Tout ce que vous avez à faire est de connaître le nom d’un assemblage qui, à votre connaissance, devrait absolument être chargé – et le transmettre à Assembly.Load . Après tout, c’est pratiquement ce que font les routines de chargement de références .Net 🙂

De même, cette approche fonctionne bien avec la détection d’assembly personnalisé mise en œuvre en suspendant l’événement AppDomain.AssemblyResolve également: Étendez les dossiers bin de l’application à tous les dossiers de conteneur de plug-ins que vous pourriez avoir afin qu’ils soient analysés. Il est toutefois probable que vous ayez déjà géré l’événement AssemblyResolve pour vous assurer qu’il sera chargé en cas d’échec de l’parsing, de sorte que tout fonctionne comme avant.

C’est ce que je fais:

 public void PreLoad() { this.AssembliesFromApplicationBaseDirectory(); } void AssembliesFromApplicationBaseDirectory() { ssortingng baseDirectory = AppDomain.CurrentDomain.BaseDirectory; this.AssembliesFromPath(baseDirectory); ssortingng privateBinPath = AppDomain.CurrentDomain.SetupInformation.PrivateBinPath; if (Directory.Exists(privateBinPath)) this.AssembliesFromPath(privateBinPath); } void AssembliesFromPath(ssortingng path) { var assemblyFiles = Directory.GetFiles(path) .Where(file => Path.GetExtension(file).Equals(".dll", SsortingngComparison.OrdinalIgnoreCase)); foreach (var assemblyFile in assemblyFiles) { // TODO: check it isnt already loaded in the app domain Assembly.LoadFrom(assemblyFile); } } 

Avez-vous essayé de regarder Assembly.GetExecutingAssembly (). Location? Cela devrait vous donner le chemin d’access à l’assembly à partir duquel votre code est exécuté. Dans le cas de NUnit, je m’attendrais à ce que les assemblys soient copiés dans l’ombre.