Cómo precarga todos los ensamblados implementados para un AppDomain


ACTUALIZACIÓN: Ahora tengo una solución con la que estoy mucho más contento, aunque no resuelvo todos los problemas sobre los que pregunto, deja el camino claro para hacerlo. He actualizado mi propia respuesta para reflejar esto.

Pregunta inicial

Dado un Dominio de Aplicación, hay muchas ubicaciones diferentes que Fusion (el cargador de ensamblajes.Net) probará para un ensamblaje determinado. Obviamente, tomamos esta funcionalidad por sentado y, ya que el sondeo parece estar incrustado dentro de la . Net runtime (Assembly._nLoad el método interno parece ser el punto de entrada cuando Reflect-Loading-y asumo que la carga implícita probablemente esté cubierta por el mismo algoritmo subyacente), ya que los desarrolladores no parecen ser capaces de obtener acceso a esas rutas de búsqueda.

Mi problema es que tengo un componente que hace una gran cantidad de resolución de tipo dinámico, y que necesita ser capaz de garantizar que todos los ensamblados implementados por el usuario para un AppDomain dado estén precargados antes de que comience su trabajo. Sí, se ralentiza down startup-pero los beneficios que obtenemos de este componente superan totalmente este.

El algoritmo de carga básico que ya he escrito es el siguiente. Analiza en profundidad un conjunto de carpetas para cualquier .DLL (.exes están siendo excluidos en este momento), y utiliza Ensamblado.LoadFrom para cargar el dll si es AssemblyName no se puede encontrar en el conjunto de ensamblados ya cargados en el AppDomain (esto se implementa de manera ineficiente, pero se puede optimizar más tarde):

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

Se utiliza LoadFrom porque he encontrado que usar Load () puede llevar a que Fusion cargue ensamblajes duplicados si, cuando lo busca, no encuentra uno cargado desde donde espera encontrarlo.

Así que, 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 usar cuando busque un ensamblado. Entonces puedo simplemente iterar a través de ellos.

El GAC es irrelevante para esto, y no estoy interesado en ninguna rutas fijas impulsadas por el entorno que Fusion podría usar, solo aquellas rutas que se pueden extraer del AppDomain que contienen ensamblados implementados expresamente para la aplicación.

Mi primera iteración de esto simplemente usó AppDomain.Dirección base. Esto funciona para servicios, aplicaciones de formularios y aplicaciones de consola.

No funciona para un Asp.Net sitio web, sin embargo, ya que hay al menos dos ubicaciones principales - el AppDomain.DynamicDirectory (donde Asp.Net coloca sus clases de página generadas dinámicamente y cualquier ensamblado al que hace referencia el código de página Aspx), y luego la carpeta Bin del sitio, que se puede descubrir desde AppDomain.Información de configuración.PrivateBinPath propiedad.

Así que ahora tengo código de trabajo para los tipos más básicos de aplicaciones ahora (los AppDomains alojados en Sql Server 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 NUnit test runner.

Esto utiliza tanto la copia de sombra (por lo que mi algoritmo tendría que ser descubrir y cargarlos desde la carpeta drop shadow-copy, no desde la carpeta bin) y configura el PrivateBinPath como relativo al directorio base.

Y, por supuesto, hay un montón de otros escenarios de alojamiento que probablemente no he considerado; pero que deben ser válidos porque de lo contrario Fusion se asfixiaría al cargar los ensamblados.

Quiero dejar de sentir alrededor y la introducción de hack sobre hack para dar cabida a estos nuevos escenarios como surgen-lo que quiero es, dado un AppDomain y su información de configuración, la capacidad de producir esta lista de Carpetas que debo escanear con el fin de recoger todos los archivos DLL que se van a cargar; independientemente de cómo el AppDomain está configurado. Si Fusion puede verlos como todos iguales, entonces también debería hacerlo mi código.

Por supuesto, podría tener que alterar el algoritmo si.Net cambia sus componentes internos, eso es solo una cruz que tendré que soportar. Igualmente, estoy feliz de considerar SQL Server y cualquier otro entornos similares a los casos perimetrales que siguen sin ser compatibles por el momento.

Cualquier idea!?

Author: Andras Zoltan, 2010-06-11

3 answers

Ahora he sido capaz de conseguir algo mucho más cerca de una solución final, excepto que todavía no está procesando la ruta de la papelera privada correctamente. He reemplazado mi código anteriormente vivo con esto y también he resuelto algunos errores desagradables de tiempo de ejecución que he tenido en el negocio (compilación dinámica de código C# haciendo referencia a demasiados archivos DLL).

La regla de oro que he descubierto desde entonces es siempre use el contexto de carga, no el contexto de carga, ya que el contexto de carga siempre será el primer lugar. Net se ve cuando se realiza un enlace natural. Por lo tanto, si usas el contexto LoadFrom, solo obtendrás un resultado si realmente lo cargas desde el mismo lugar desde el que se enlazaría naturalmente, lo cual no siempre es fácil.

Esta solución funciona tanto para aplicaciones web, teniendo en cuenta la diferencia de carpetas bin frente a aplicaciones 'estándar'. Se puede extender fácilmente para acomodar el problema PrivateBinPath, una vez que pueda obtener un control confiable sobre exactamente cómo es ¡lee!)

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 se habrán desplegado los ensamblados desplegados por el usuario. Es unEnumerable debido a la caja de borde PrivateBinPath (puede ser una serie de ubicaciones), pero en la práctica solo es una carpeta en este momento:

El siguiente método es PreLoadDeployedAssemblies(), que se llama antes de hacer nada (aquí se enumera como private static - en mi código esto se toma de una clase estática mucho más grande que tiene endpoints públicos que siempre activarán este código para que se ejecute antes de hacer nada por primera vez.

Finalmente está la carne y los huesos. Lo más importante aquí es tomar un archivo ensamblador y obtener su nombre de ensamblaje, que luego pasan a Assembly.Load(AssemblyName) - y no usar LoadFrom.

Anteriormente pensé que LoadFrom era más confiable, y que tenía que ir manualmente y encontrar el temporal Asp.Net carpeta en aplicaciones web. Todo lo que tienes que hacer es conoce el nombre de un ensamblado que sabes que definitivamente debería ser cargado-y pásalo a Assembly.Load. Después de todo, eso es prácticamente lo que hacen las rutinas de carga de referencia de. Net:)

Igualmente, este enfoque funciona muy bien con el sondeo de ensamblaje personalizado implementado colgando del evento AppDomain.AssemblyResolve también: Extienda las carpetas bin de la aplicación a cualquier carpeta contenedor de complementos que pueda tener para que se escaneen. Lo más probable es que ya haya manejado el evento AssemblyResolve de todos modos para asegurarse de que se cargan cuando el el sondeo normal falla, así que todo funciona como antes.

 19
Author: Andras Zoltan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-01-18 23:32:34

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);
    }
}
 4
Author: Andrew Bullock,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-11-02 16:54:06

¿Has intentado mirar el Montaje?GetExecutingAssembly().Ubicación? Eso debería darle la ruta al ensamblado desde donde se ejecuta su código. En el caso NUnit, esperaría que fuera donde las asambleas fueron copiadas en la sombra.

 0
Author: Andy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-06-17 04:01:26