Gibt es eine Möglichkeit, das Laden aller referenzierten Assemblys in die App-Domäne zu erzwingen?

82

Meine Projekte sind wie folgt aufgebaut:

  • Projektdefinition"
  • Projekt "Implementierung"
  • Projekt "Verbraucher"

Das Projekt "Consumer" verweist sowohl auf "Definition" als auch auf "Implementation", verweist jedoch statisch nicht auf Typen in "Implementation".

Wenn die Anwendung gestartet wird, ruft das Projekt "Consumer" in "Definition" eine statische Methode auf, die in "Implementierung" Typen finden muss.

Gibt es eine Möglichkeit, das Laden einer Assembly, auf die verwiesen wird, in die App-Domäne zu erzwingen, ohne den Pfad oder Namen zu kennen und vorzugsweise ohne ein vollwertiges IOC-Framework verwenden zu müssen?

Daniel Schaffer
quelle
1
Was für ein Problem verursacht es? Warum müssen Sie das Laden erzwingen?
Mike Two
Es wird überhaupt nicht geladen, vermutlich weil es keine statische Abhängigkeit gibt
Daniel Schaffer
Wie versuchen Sie, Typen in der Implementierung zu finden? Suchen Sie etwas, das eine bestimmte Schnittstelle implementiert?
Mike Two
2
@ Mike: Ja. Ich mache AppDomain.CurrentDomain.GetAssemblies und benutze eine Linq-Abfrage, um GetTypes () für jeden von ihnen rekursiv aufzurufen.
Daniel Schaffer

Antworten:

90

Dies schien den Trick zu tun:

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

Wie Jon bemerkte, müsste die ideale Lösung in die Abhängigkeiten für jede der geladenen Assemblys zurückgreifen, aber in meinem speziellen Szenario muss ich mir darüber keine Sorgen machen.


Update: Das in .NET 4 enthaltene Managed Extensibility Framework (System.ComponentModel) bietet viel bessere Möglichkeiten, um solche Dinge zu erreichen.

Daniel Schaffer
quelle
5
Dies funktioniert nicht für mich, meine referenzierten Assemblys, die nicht geladen sind, werden nicht in AppDomain.CurrentDomain.GetAssemblies () angezeigt. Hmm ...
Ted
11
Welche Einrichtungen? Ich habe nichts über die Suche gefunden.
Nuzzolilo
8
Mit MEF kann der obige Code wie folgt gekürzt werden: new DirectoryCatalog(".");(Referenzierung erforderlich System.ComponentModel.Composition).
Allon Guralnek
1
Diese Antwort löste mein Problem und arbeitete für mich. Ich hatte ein MS-Unit-Test-Projekt, das auf eine andere meiner Assemblys verwies, und AppDomain.CurrentDomain.GetAssemblies () gab diese Assembly beim Ausführen eines Tests nicht zurück. Ich vermute, dass, obwohl meine Komponententests Code aus dieser Bibliothek verwendeten, die Assembly möglicherweise nicht in "GetAssemblies" angezeigt wurde, da vs.net ein MS-Komponententestprojekt (Klassenbibliothek) im Vergleich zur Ausführung eines regulären Projekts lädt .exe Anwendung. Beachten Sie Folgendes, wenn Ihr Code Reflektion verwendet und Ihre Komponententests nicht bestehen.
Dean Lunz
4
Ich wollte nur hinzufügen, seien Sie vorsichtig mit dynamisch geladenen Assemblys. Das aufgerufene Mitglied wird in einer dynamischen Assembly nicht unterstützt. Filtern Sie entweder Assemblys heraus, bei denen IsDynamic = false ist, oder versuchen Sie, Ihren Aufruf von CurrentDomain.Load abzufangen, wenn Sie fehlertolerant gegenüber Lasten sind. Und assembly.Location. Das muss man auch überprüfen. Funktioniert nicht für IsDynamicBaugruppen.
Eli Gassert
63

Sie können verwenden Assembly.GetReferencedAssemblies, um eine zu erhalten AssemblyName[], und dann Assembly.Load(AssemblyName)jeden von ihnen aufrufen . Sie müssen natürlich wiederkehren - aber am besten behalten Sie die Baugruppen im Auge, die Sie bereits geladen haben :)

Jon Skeet
quelle
Ich habe das gefunden, aber das Problem ist, dass ich alles tun muss, was ich von der referenzierten Assembly aus mache ... und zumindest im Rahmen eines Komponententests geben GetCallingAssembly, GetExecutingAssembly natürlich die referenzierte Assembly zurück und GetEntryAssembly gibt null zurück : \
Daniel Schaffer
4
Wenn Sie nach dem Laden von Referenzbaugruppen das oben genannte Problem lösen. Sie können auch einen bestimmten Typ (T) .Assembly fragen, ob dies hilfreich ist. Ich habe das Gefühl, dass Sie die Assemblys, die die Implementierung enthalten (nicht referenziert), dynamisch laden müssen. In diesem Fall müssen Sie entweder eine statische Namensliste führen und diese manuell laden oder Ihr gesamtes Verzeichnis durchsuchen, den Typ mit den richtigen Schnittstellen laden und dann suchen.
Fadrian Sudaman
1
@vanhelgen: Nach meiner Erfahrung ist es selten etwas, das Sie explizit benötigen. Normalerweise funktioniert die "Load on Demand" der CLR einwandfrei.
Jon Skeet
2
Unter normalen Umständen mag dies zutreffen, aber wenn ein DI-Container zum Ermitteln verfügbarer Dienste (über System.Reflection) verwendet wird, werden die in Baugruppen enthaltenen Dienste, die noch nicht geladen wurden, natürlich nicht gefunden. Seitdem habe ich standardmäßig eine Dummy-Unterklasse aus einem zufälligen Typ jeder referenzierten Assembly im CompositionRoot meiner App erstellt, um sicherzustellen, dass alle Abhängigkeiten vorhanden sind. Ich hoffe, ich kann diesen Unsinn überspringen, indem ich alles im Voraus lade, auch auf Kosten einer weiteren Verlängerung der Startzeit. @ JonSkeet gibt es eine andere Möglichkeit, dies zu tun? thx
mfeineis
12
Dies führt dazu, dass GetReferencedAssemblies anscheinend eine "optimierte" Liste zurückgibt. Wenn Sie also in einer Assembly, auf die verwiesen wird, keinen expliziten Code aufrufen, wird dieser nicht berücksichtigt. (
Gemäß
23

wollte nur ein rekursives Beispiel teilen. Ich rufe die LoadReferencedAssembly-Methode in meiner Startroutine folgendermaßen auf:

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

Dies ist die rekursive Methode:

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
quelle
5
Ich frage mich, ob kreisförmige Assembly-Referenzen dazu führen können, dass Stapelüberlauf-Ausnahmen ausgelöst werden.
Ronnie Overby
1
Ronnie, ich glaube nicht, wird der Code nur die Rekursion , falls namenicht bereits AppDomain.CurrentDomain.GetAssemblies(), was bedeutet , dass es nur wiederholen, wenn das foreachgepflückt AssemblyNamenoch nicht geladen ist .
Felype
1
Ich bin nicht glücklich über die O(n^2)Laufzeit dieses Algorithmus ( GetAssemblies().Any(...)innerhalb von a foreach)). Ich würde ein verwenden HashSet, um dies auf etwas in der Größenordnung von zu bringen O(n).
Dai
16

Wenn Sie Fody.Costura oder eine andere Lösung zum Zusammenführen von Baugruppen verwenden, funktioniert die akzeptierte Antwort nicht.

Im Folgenden werden die referenzierten Baugruppen einer aktuell geladenen Baugruppe geladen. Die Rekursion bleibt Ihnen überlassen.

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
quelle
Möchten Sie wissen, wohin dieses Snippet gehen soll?
Telemat
1
in deinem Bootloader / Start stelle ich mir vor.
Meirion Hughes
1
Ich kann mich irren, aber ich denke, Sie können einfach !y.IsDynamicin Ihrem.Where
Felype
1

Da ich heute eine Assembly + Abhängigkeiten von einem bestimmten Pfad laden musste, habe ich diese Klasse geschrieben, um dies zu tun.

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
quelle
1
Guter Code, außer dass das Wörterbuch nicht benötigt wird. In diesem Fall würde eine einfache Liste ausreichen. Ich gehe davon aus, dass Ihr ursprünglicher Code wissen musste, welche Assemblys geladen wurden und welche nicht. Deshalb haben Sie das Wörterbuch.
Reinis
0

Eine weitere Version (basierend auf der Antwort von Daniel Schaffer ) ist der Fall, wenn Sie möglicherweise nicht alle Baugruppen laden müssen, sondern eine vordefinierte Anzahl davon:

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
quelle
0

Wenn Sie Assemblys haben, auf die zur Kompilierungszeit kein Code referenziert wird, werden diese Assemblys nicht als Referenz auf Ihre andere Assembly aufgenommen, selbst wenn Sie das Projekt- oder Nuget-Paket als Referenz hinzugefügt haben. Dies ist unabhängig von Debugoder ReleaseBuild-Einstellungen, Code-Optimierung usw. In diesen Fällen müssen Sie explizit aufrufen Assembly.LoadFrom(dllFileName), um die Assembly zu laden.

jjxtra
quelle
0

Um die referenzierte Assembly nach Namen zu erhalten, können Sie die folgende Methode verwenden:

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
quelle
0

In meiner Winforms-Anwendung gebe ich JavaScript (in einem WebView2-Steuerelement) die Möglichkeit, verschiedene .NET-Dinge aufzurufen, beispielsweise Methoden Microsoft.VisualBasic.Interactionin der Assembly Microsoft.VisualBasic.dll (zInputBox() usw.).

Meine Anwendung als solche verwendet diese Assembly jedoch nicht, sodass die Assembly nie geladen wird.

Um das Laden der Assembly zu erzwingen, habe ich dies einfach in Form1_Load hinzugefügt:

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

Der Compiler glaubt, dass die Assembly möglicherweise benötigt wird, aber in Wirklichkeit passiert dies natürlich nie.

Keine sehr ausgefeilte Lösung, aber schnell und schmutzig.

Magnus
quelle