c# - what - Cómo precargar todos los ensamblados desplegados para un AppDomain
system reflection c# (3)
¿Has probado en Assembly.GetExecutingAssembly (). Location? Eso debería darle la ruta al ensamblado desde el que se ejecuta el código. En el caso de NUnit, esperaría que estuviera donde se copiaron las asambleas.
ACTUALIZACIÓN: ahora tengo una solución, estoy mucho más feliz con eso, aunque no resuelvo todos los problemas que pido, deja el camino libre para hacerlo. He actualizado mi propia respuesta para reflejar esto.
Pregunta original
Dado un dominio de aplicación, hay muchas ubicaciones diferentes que Fusion (el cargador de conjuntos de .Net) buscará para un ensamblaje determinado. Obviamente, damos por hecha esta funcionalidad y, dado que el sondeo parece estar incrustado dentro del tiempo de ejecución .Net (el método interno Assembly._nLoad
parece ser el punto de entrada cuando Reflect-Loading - y supongo que la carga implícita probablemente esté cubierta por el mismo algoritmo subyacente), como desarrolladores, no parece que podamos obtener acceso a esas rutas de búsqueda.
Mi problema es que tengo un componente que tiene mucha resolución de tipo dinámico, y que necesita para poder garantizar que todos los ensamblados implementados por el usuario para un Dominio de aplicación determinado estén precargados antes de que comience su trabajo. Sí, ralentiza el inicio, pero los beneficios que obtenemos de este componente superan por completo a este.
El algoritmo básico de carga que ya he escrito es el siguiente. Analiza en profundidad un conjunto de carpetas para cualquier archivo .dll (se excluyen .exes en este momento ) y usa Assembly.LoadFrom para cargar el dll si no se puede encontrar AssemblyName en el conjunto de ensamblados ya cargados en el AppDomain (esto se implementa de manera ineficiente, pero se puede optimizar más adelante):
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 se utiliza porque he encontrado que usar Load () puede llevar a Fusion a cargar ensamblajes duplicados si, cuando lo busca, no encuentra uno cargado desde donde espera encontrarlo.
Entonces, con esto en su lugar, todo lo que tengo que hacer ahora es obtener una lista en orden de precedencia (de mayor a menor) de las rutas de búsqueda que Fusion va a utilizar cuando busque un ensamblaje. Entonces puedo simplemente iterar a través de ellos.
El GAC es irrelevante para esto, y no me interesan las rutas fijas controladas por el entorno que Fusion pueda usar, solo aquellas rutas que se pueden obtener del AppDomain que contiene ensamblajes desplegados expresamente para la aplicación.
Mi primera iteración de esto simplemente usó AppDomain.BaseDirectory. Esto funciona para servicios, aplicaciones de consola y forma.
Sin embargo, no funciona para un sitio web Asp.Net, ya que hay al menos dos ubicaciones principales: AppDomain.DynamicDirectory (donde Asp.Net coloca sus clases de página generadas dinámicamente y cualquier ensamblaje al que haga referencia el código de la página Aspx), y luego, la carpeta Bin del sitio, que se puede descubrir desde la propiedad AppDomain.SetupInformation.PrivateBinPath.
Ahora tengo código de trabajo para los tipos más básicos de aplicaciones (los dominios de aplicación Sql alojados en servidor son otra historia ya que el sistema de archivos está virtualizado), pero me encontré con un problema interesante hace un par de días donde este código simplemente no funciona : el corredor de prueba nUnit.
Esto usa tanto Shadow Copying (por lo que mi algoritmo debería estar descubriendo y cargándolos desde la carpeta de instantáneas, no desde la carpeta bin) y configura PrivateBinPath como relativo al directorio base.
Y, por supuesto, hay muchos otros escenarios de alojamiento que probablemente no he considerado; pero que debe ser válido porque de lo contrario Fusion se ahogaría al cargar los ensamblajes.
Quiero dejar de sentir e introducir el hack sobre hack para acomodar estos nuevos escenarios a medida que surgen, lo que quiero es que, dado un AppDomain y su información de configuración, la capacidad de generar esta lista de carpetas que deba escanear para poder elegir todas las DLL que se van a cargar; independientemente de cómo esté configurado el AppDomain. Si Fusion puede verlos de todos modos, entonces también debería hacerlo mi código.
Por supuesto, podría tener que alterar el algoritmo si .Net cambia sus aspectos internos, es solo una cruz que tendré que soportar. Igualmente, estoy feliz de considerar SQL Server y cualquier otro entorno similar como edge-cases que por ahora no están soportados.
¿¡Algunas ideas!?
Ahora he podido obtener algo mucho más cercano a una solución final, excepto que todavía no está procesando la ruta de la papelera privada correctamente. He reemplazado mi código anterior en vivo con esto y también he resuelto algunos desagradables errores de tiempo de ejecución que he tenido en la negociación (compilación dinámica de código C # que hace referencia a demasiados dlls).
La regla de oro que he descubierto siempre es usar el contexto de carga , no el contexto LoadFrom, ya que el contexto Load siempre será el primer lugar que se ve .Net cuando se realiza un enlace natural. Por lo tanto, si utilizas el contexto LoadFrom, solo recibirás un golpe si realmente lo cargas desde el mismo lugar desde el que naturalmente lo vincularía, lo que no siempre es fácil.
Esta solución funciona tanto para aplicaciones web, teniendo en cuenta la diferencia de la carpeta bin frente a las aplicaciones ''estándar''. Se puede extender fácilmente para acomodar el problema de PrivateBinPath
, una vez que pueda obtener un manejo confiable de cómo se lee exactamente (!)
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);
}
}
}
Primero tenemos el método utilizado para recuperar nuestras ''carpetas de aplicaciones'' elegidas. Estos son los lugares donde los ensambles desplegados por el usuario se habrán desplegado. Es un IEnumerable debido a la PrivateBinPath
borde de PrivateBinPath
(puede tratarse de una serie de ubicaciones), pero en la práctica es solo una carpeta en este momento:
El siguiente método es PreLoadDeployedAssemblies()
, que se llama antes de hacer cualquier cosa (aquí está listado como private static
- en mi código esto se toma de una clase estática mucho más grande que tiene puntos finales públicos que siempre activarán este código antes de hacer cualquier cosa por primera vez.
Finalmente está la carne y los huesos. Lo más importante aquí es tomar un archivo de ensamblado y obtener su nombre de ensamblado , que luego pasará a Assembly.Load(AssemblyName)
y no usar LoadFrom
.
Anteriormente pensé que LoadFrom
era más confiable y que tenía que buscar y buscar manualmente la carpeta Asp.Net temporal en las aplicaciones web. Tu no Todo lo que tiene que hacer es saber el nombre de un ensamblaje que sabe que definitivamente debe cargarse y pasarlo a Assembly.Load
. Después de todo, eso es prácticamente lo que hacen las rutinas de carga de referencia de .Net :)
De la misma manera, este enfoque funciona muy bien con el sondeo de ensamblaje personalizado implementado colgando el evento AppDomain.AssemblyResolve
también: Extienda las carpetas bin de la aplicación a cualquier carpeta de contenedor de plugins que pueda tener para que se escaneen. Lo más probable es que ya haya manejado el evento AssemblyResolve
todos modos para asegurarse de que se carguen cuando falla el sondeo normal, por lo que todo funciona igual que antes.
Esto es lo que hago:
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);
}
}