¿Hay alguna manera de forzar que todos los ensamblados referenciados se carguen en el dominio de la aplicación?

83

Mis proyectos están configurados así:

  • Definición del proyecto"
  • Implementacion de proyecto"
  • Proyecto "Consumidor"

El proyecto "Consumidor" hace referencia tanto a la "Definición" como a la "Implementación", pero no hace referencia estática a ningún tipo en "Implementación".

Cuando se inicia la aplicación, el proyecto "Consumidor" llama a un método estático en "Definición", que necesita encontrar tipos en "Implementación".

¿Hay alguna manera de forzar que cualquier ensamblado referenciado se cargue en el dominio de la aplicación sin conocer la ruta o el nombre, y preferiblemente sin tener que usar un marco IOC completo?

Daniel Schaffer
fuente
1
¿Qué tipo de problema está causando? ¿Por qué necesita forzar la carga?
Mike Two
No se carga en absoluto, presumiblemente porque no hay dependencia estática
Daniel Schaffer
¿Cómo intenta "encontrar tipos" en la implementación? ¿Está buscando algo que implemente una interfaz específica?
Mike Two
2
@ Mike: Sí. Estoy haciendo AppDomain.CurrentDomain.GetAssemblies, y uso una consulta linq para llamar de forma recursiva a GetTypes () en cada uno de ellos.
Daniel Schaffer

Respuestas:

90

Esto parecía hacer el truco:

var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies().ToList();
var loadedPaths = loadedAssemblies.Select(a => a.Location).ToArray();

var referencedPaths = Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "*.dll");
var toLoad = referencedPaths.Where(r => !loadedPaths.Contains(r, StringComparer.InvariantCultureIgnoreCase)).ToList();

toLoad.ForEach(path => loadedAssemblies.Add(AppDomain.CurrentDomain.Load(AssemblyName.GetAssemblyName(path))));

Como señaló Jon, la solución ideal tendría que recurrir a las dependencias para cada uno de los ensamblados cargados, pero en mi escenario específico no tengo que preocuparme por eso.


Actualización: El marco de extensibilidad administrado (System.ComponentModel) incluido en .NET 4 tiene instalaciones mucho mejores para lograr cosas como esta.

Daniel Schaffer
fuente
5
Esto no funciona para mí, mis ensamblajes referenciados, que no están cargados, no aparecen en AppDomain.CurrentDomain.GetAssemblies () .. Hmm ...
Ted
11
¿Qué instalaciones? No he encontrado nada buscando.
nuzzolilo
8
Usando MEF, el código anterior se puede acortar a: new DirectoryCatalog(".");(requiere referencia System.ComponentModel.Composition).
Allon Guralnek
1
Esta respuesta resolvió mi problema y funcionó para mí. Tenía un proyecto de prueba de unidad de MS que hacía referencia a otro de mis ensamblajes y AppDomain.CurrentDomain.GetAssemblies () no devolvía ese ensamblaje cuando ejecutaba una prueba. Sospecho que aunque mis pruebas unitarias estaban usando código de esa biblioteca, es posible que el ensamblado no se haya mostrado en "GetAssemblies" debido a la forma en que vs.net carga un proyecto de prueba unitaria de MS (biblioteca de clases) en comparación con la ejecución de un Aplicación .exe. Algo a tener en cuenta si su código utiliza la reflexión y no supera las pruebas unitarias.
Dean Lunz
4
Solo quería agregar, tenga cuidado con los ensamblados cargados dinámicamente El miembro invocado no es compatible con un ensamblado dinámico. Filtre los ensamblados donde IsDynamic = false, o si puede ser tolerante a fallas de cargas, intente / capture su llamada a CurrentDomain.Load. Y assembly.Location. Ese también necesita ser verificado. No funciona para IsDynamicensamblajes.
Eli Gassert
63

Puede usar Assembly.GetReferencedAssembliespara obtener un AssemblyName[], y luego llamar Assembly.Load(AssemblyName)a cada uno de ellos. Tendrá que recurrir, por supuesto, pero preferiblemente realizar un seguimiento de los ensamblados que ya ha cargado :)

Jon Skeet
fuente
Encontré eso, pero el problema es que tengo que hacer lo que sea que esté haciendo desde el ensamblado al que se hace referencia ... y al menos en el contexto de una prueba unitaria, GetCallingAssembly, GetExecutingAssembly, por supuesto, devuelven el ensamblado al que se hace referencia, y GetEntryAssembly devuelve nulo : \
Daniel Schaffer
4
Si desea cargar conjuntos de referencia, lo anterior resolverá su problema. También puede solicitar un tipo específico de tipo de (T). Ensamblaje si eso ayuda. Tengo la sensación de que lo que necesita es cargar dinámicamente los ensamblados que contienen la implementación (no referenciados). Si este es el caso, tendrá que mantener una lista estática de nombres y cargarlos manualmente o revisar todo su directorio, cargar y luego encontrar el tipo con las interfaces correctas.
Fadrian Sudaman
1
@vanhelgen: En mi experiencia, rara vez es algo que necesites hacer explícitamente. Normalmente, la "carga bajo demanda" del CLR funciona bien.
Jon Skeet
2
En circunstancias normales, eso puede ser cierto, pero cuando se usa un contenedor DI para descubrir servicios disponibles (a través de System.Reflection), naturalmente no encuentra los servicios contenidos en ensamblados que aún no se han cargado. Mi enfoque predeterminado desde entonces ha sido crear una subclase ficticia a partir de un tipo aleatorio de cada ensamblado referenciado en CompositionRoot de mi aplicación para asegurarme de que todas las dependencias estén en su lugar. Espero poder omitir esta falta de sentido cargando todo por adelantado, incluso a costa de aumentar aún más el tiempo de inicio. @JonSkeet, ¿hay otra forma de hacer esto? thx
mfeineis
12
Donde esto falla es que GetReferencedAssemblies aparentemente devuelve una lista "optimizada", por lo que si no llama explícitamente al código en un ensamblado referenciado, no se incluirá. (Según esta discusión )
FTWinston
23

solo quería compartir un ejemplo recursivo. Estoy llamando al método LoadReferencedAssembly en mi rutina de inicio de esta manera:

foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
{
    this.LoadReferencedAssembly(assembly);
}

Este es el método recursivo:

private void LoadReferencedAssembly(Assembly assembly)
{
    foreach (AssemblyName name in assembly.GetReferencedAssemblies())
    {
        if (!AppDomain.CurrentDomain.GetAssemblies().Any(a => a.FullName == name.FullName))
        {
            this.LoadReferencedAssembly(Assembly.Load(name));
        }
    }
}
jmelhus
fuente
5
Me pregunto si las referencias de ensamblaje circular podrían provocar que se produzcan excepciones de desbordamiento de pila.
Ronnie Overby
1
Ronnie, creo que no, el código solo ejecuta la recursividad en caso de nameque no esté ya AppDomain.CurrentDomain.GetAssemblies(), lo que significa que solo se repetirá si el foreachelegido AssemblyNameaún no está cargado.
Felype
1
No estoy contento con el O(n^2)tiempo de ejecución de este algoritmo ( GetAssemblies().Any(...)dentro de a foreach)). Usaría un HashSetpara reducir esto a algo del orden de O(n).
Dai
16

Si usa Fody.Costura o cualquier otra solución de fusión de ensamblajes, la respuesta aceptada no funcionará.

Lo siguiente carga los ensamblajes referenciados de cualquier ensamblaje cargado actualmente. La recursividad se te deja a ti.

var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies().ToList();

loadedAssemblies
    .SelectMany(x => x.GetReferencedAssemblies())
    .Distinct()
    .Where(y => loadedAssemblies.Any((a) => a.FullName == y.FullName) == false)
    .ToList()
    .ForEach(x => loadedAssemblies.Add(AppDomain.CurrentDomain.Load(x)));
Meirion Hughes
fuente
¿Le importaría indicar dónde debe ir este fragmento?
Telemat
1
en su cargador de arranque / inicio, imagino.
Meirion Hughes
1
Puede que me equivoque, pero creo que puede comprobarlo !y.IsDynamicen su.Where
Felype
1

Viendo que tenía que cargar un ensamblado + dependencias de una ruta específica hoy, escribí esta clase para hacerlo.

public static class AssemblyLoader
{
    private static readonly ConcurrentDictionary<string, bool> AssemblyDirectories = new ConcurrentDictionary<string, bool>();

    static AssemblyLoader()
    {
        AssemblyDirectories[GetExecutingAssemblyDirectory()] = true;
        AppDomain.CurrentDomain.AssemblyResolve += ResolveAssembly;

    }

    public static Assembly LoadWithDependencies(string assemblyPath)
    {
        AssemblyDirectories[Path.GetDirectoryName(assemblyPath)] = true;
        return Assembly.LoadFile(assemblyPath);
    }

    private static Assembly ResolveAssembly(object sender, ResolveEventArgs args)
    {
        string dependentAssemblyName = args.Name.Split(',')[0] + ".dll";
        List<string> directoriesToScan = AssemblyDirectories.Keys.ToList();

        foreach (string directoryToScan in directoriesToScan)
        {
            string dependentAssemblyPath = Path.Combine(directoryToScan, dependentAssemblyName);
            if (File.Exists(dependentAssemblyPath))
                return LoadWithDependencies(dependentAssemblyPath);
        }
        return null;
    }

    private static string GetExecutingAssemblyDirectory()
    {
        string codeBase = Assembly.GetExecutingAssembly().CodeBase;
        var uri = new UriBuilder(codeBase);
        string path = Uri.UnescapeDataString(uri.Path);
        return Path.GetDirectoryName(path);
    }
}
Peter Morris
fuente
1
buen código, excepto que no es necesario el Diccionario, en este caso una simple lista sería suficiente. Supongo que su código original necesitaba saber qué ensamblajes se cargaron y cuáles no, por lo que tiene el Diccionario.
Reinis
0

Sin embargo, otra versión (basada en la respuesta de Daniel Schaffer ) es el caso en el que es posible que no necesite cargar todos los ensamblados, sino un número predefinido de ellos:

var assembliesToLoad = { "MY_SLN.PROJECT_1", "MY_SLN.PROJECT_2" };

// First trying to get all in above list, however this might not 
// load all of them, because CLR will exclude the ones 
// which are not used in the code
List<Assembly> dataAssembliesNames =
   AppDomain.CurrentDomain.GetAssemblies()
            .Where(assembly => AssembliesToLoad.Any(a => assembly.GetName().Name == a))
            .ToList();

var loadedPaths = dataAssembliesNames.Select(a => a.Location).ToArray();

var compareConfig = StringComparison.InvariantCultureIgnoreCase;
var referencedPaths = Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "*.dll")
    .Where(f =>
    {
       // filtering the ones which are in above list
       var lastIndexOf = f.LastIndexOf("\\", compareConfig);
       var dllIndex = f.LastIndexOf(".dll", compareConfig);

       if (-1 == lastIndexOf || -1 == dllIndex)
       {
          return false;
       }

       return AssembliesToLoad.Any(aName => aName == 
          f.Substring(lastIndexOf + 1, dllIndex - lastIndexOf - 1));
     });

var toLoad = referencedPaths.Where(r => !loadedPaths.Contains(r, StringComparer.InvariantCultureIgnoreCase)).ToList();

toLoad.ForEach(path => dataAssembliesNames.Add(AppDomain.CurrentDomain.Load(AssemblyName.GetAssemblyName(path))));

if (dataAssembliesNames.Count() != AssembliesToLoad.Length)
{
   throw new Exception("Not all assemblies were loaded into the  project!");
}
Arsen Khachaturyan
fuente
0

Si tiene ensamblados en los que no se hace referencia a ningún código en tiempo de compilación, esos ensamblados no se incluirán como referencia a su otro ensamblado, incluso si ha agregado el proyecto o el paquete nuget como referencia. Esto es independientemente de Debugo Releaseconfiguración, optimización de código, etc. En estos casos la acumulación, debe llamar explícitamente Assembly.LoadFrom(dllFileName)para obtener el ensamblado cargado.

jjxtra
fuente
0

Para obtener el ensamblaje referenciado por nombre, puede usar el siguiente método:

public static Assembly GetAssemblyByName(string name)
{
    var asm = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.FullName == name);
    if (asm == null)
        asm = AppDomain.CurrentDomain.Load(name);
    return asm;
}
Petr Voborník
fuente
0

En mi aplicación winforms le doy a JavaScript (en un control WebView2) la posibilidad de llamar a varias cosas .NET, por ejemplo, métodos Microsoft.VisualBasic.Interactionen el ensamblado Microsoft.VisualBasic.dll (comoInputBox() etc.).

Pero mi aplicación como tal no usa ese ensamblado, por lo que el ensamblado nunca se carga.

Entonces, para forzar la carga del ensamblaje, terminé simplemente agregando esto en mi Form1_Load:

if (DateTime.Now < new DateTime(1000, 1, 1, 0, 0, 0)) { // never happens
  Microsoft.VisualBasic.Interaction.Beep();
  // you can add more things here
}

El compilador piensa que el ensamblaje podría ser necesario, pero en realidad esto nunca sucede, por supuesto.

No es una solución muy sofisticada, pero rápida y sucia.

Magnus
fuente