Czy istnieje sposób, aby wymusić załadowanie wszystkich zestawów, do których istnieją odwołania, do domeny aplikacji?

83

Moje projekty są ustawione w następujący sposób:

  • Definicja projektu"
  • Wdrożenie projektu"
  • Projekt „Konsument”

Projekt „Konsument” odwołuje się zarówno do „Definicji”, jak i „Wdrażania”, ale nie zawiera statycznego odniesienia do żadnego typu w polu „Wdrożenie”.

Po uruchomieniu aplikacji Projekt „Konsument” wywołuje metodę statyczną w „Definicji”, która musi znaleźć typy w polu „Implementacja”

Czy istnieje sposób, w jaki mogę wymusić załadowanie dowolnego zestawu, do którego się odwołujemy, do domeny aplikacji bez znajomości ścieżki lub nazwy, a najlepiej bez konieczności korzystania z pełnoprawnej struktury IOC?

Daniel Schaffer
źródło
1
Jaki problem to powoduje? Dlaczego musisz wymusić ładowanie?
Mike Two,
W ogóle się nie ładuje, prawdopodobnie dlatego, że nie ma statycznej zależności
Daniel Schaffer
W jaki sposób próbujesz „znaleźć typy” w implementacji? Szukasz czegoś, co implementuje określony interfejs?
Mike Two
2
@Mike: Tak. Robię AppDomain.CurrentDomain.GetAssemblies i używam zapytania linq do rekurencyjnego wywołania GetTypes () na każdym z nich.
Daniel Schaffer

Odpowiedzi:

90

Wydawało się, że to działa:

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

Jak zauważył Jon, idealne rozwiązanie wymagałoby ponownego użycia zależności dla każdego z załadowanych zestawów, ale w moim scenariuszu nie muszę się tym martwić.


Aktualizacja: Managed Extensibility Framework (System.ComponentModel) zawarty w .NET 4 ma znacznie lepsze możliwości wykonywania takich rzeczy.

Daniel Schaffer
źródło
5
To nie działa dla mnie, moje przywoływane zestawy, które nie są załadowane, nie pojawiają się w AppDomain.CurrentDomain.GetAssemblies () .. Hmm ...
Ted
11
Jakie udogodnienia? Nie znalazłem nic poprzez wyszukiwanie.
nuzzolilo
8
Używając MEF, powyższy kod można skrócić do: new DirectoryCatalog(".");(wymaga odwołania System.ComponentModel.Composition).
Allon Guralnek
1
Ta odpowiedź rozwiązała mój problem i zadziałała dla mnie. Miałem projekt testu jednostkowego MS odwołujący się do innego z moich zestawów i AppDomain.CurrentDomain.GetAssemblies () nie zwracał tego zestawu podczas uruchamiania testu. Podejrzewam, że chociaż moje testy jednostkowe korzystały z kodu z tej biblioteki, zespół mógł nie pojawiać się w „GetAssemblies” ze względu na sposób, w jaki w porównaniu z siecią ładuje się projekt testu jednostkowego MS (biblioteka klas) w porównaniu z uruchomieniem zwykłego Aplikacja .exe. Coś, o czym należy pamiętać, jeśli kod używa odbicia i nie przechodzi testów jednostkowych.
Dean Lunz
4
Chciałem tylko dodać, uważaj na zestawy ładowane dynamicznie. Wywołany element członkowski nie jest obsługiwany w zestawie dynamicznym. Odfiltruj zestawy, w których IsDynamic = false, lub jeśli możesz być odporny na błędy w obciążeniach, spróbuj / złap wywołanie CurrentDomain.Load. A assembly.Location. To też trzeba sprawdzić. Nie działa w przypadku IsDynamiczespołów.
Eli Gassert,
63

Możesz użyć, Assembly.GetReferencedAssembliesaby uzyskać AssemblyName[], a następnie zadzwonić Assembly.Load(AssemblyName)do każdego z nich. Oczywiście będziesz musiał powtórzyć - ale najlepiej śledzić zestawy, które już załadowałeś :)

Jon Skeet
źródło
Znalazłem to, ale problem polega na tym, że muszę zrobić wszystko, co robię z przywoływanego zestawu ... i przynajmniej w kontekście testu jednostkowego, GetCallingAssembly, GetExecutingAssembly oczywiście zwracają zestaw, do którego się odwołujesz, a GetEntryAssembly zwraca wartość null : \
Daniel Schaffer
4
Jeśli jesteś po załadowaniu zestawów referencyjnych, powyższe rozwiąże Twój problem. Możesz również zapytać o konkretny typ zespołu (T), jeśli to pomoże. Mam wrażenie, że to, czego potrzebujesz, to dynamiczne ładowanie zestawów zawierających implementację (bez odwołań). W takim przypadku będziesz musiał albo zachować statyczną listę nazw i załadować je ręcznie, albo przeglądać cały katalog, załadować, a następnie znaleźć typ z odpowiednimi interfejsami.
Fadrian Sudaman
1
@vanhelgen: Z mojego doświadczenia wynika, że ​​rzadko jest to coś, co trzeba wyraźnie określić. Zwykle ładowanie na żądanie CLR działa dobrze.
Jon Skeet
2
W normalnych okolicznościach może to być prawda, ale w przypadku używania kontenera DI do wykrywania dostępnych usług (za pośrednictwem System.Reflection) naturalnie nie znajduje usług zawartych w zestawach, które nie zostały jeszcze załadowane. Od tamtej pory moim domyślnym podejściem jest utworzenie fałszywej podklasy z losowego typu każdego zestawu, do którego się odwołujemy w CompositionRoot mojej aplikacji, aby upewnić się, że wszystkie zależności są na miejscu. Mam nadzieję, że uda mi się pominąć ten nonsens, ładując wszystko z góry, nawet kosztem dalszego wydłużania czasu uruchamiania. @JonSkeet czy jest inny sposób na zrobienie tego? thx
mfeineis
12
Tam, gdzie to spada, jest to, że GetReferencedAssemblies najwyraźniej zwraca „zoptymalizowaną” listę - więc jeśli nie wywołasz jawnie kodu w zestawie, do którego się odwołuje, nie zostanie on uwzględniony. (W tej dyskusji )
FTWinston
23

chciałem tylko udostępnić przykład rekurencyjny. Wywołuję metodę LoadReferencedAssembly w mojej procedurze uruchamiania w następujący sposób:

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

Oto metoda rekurencyjna:

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
źródło
5
Zastanawiam się, czy cykliczne odwołania do zestawów mogą powodować wyrzucanie wyjątków przepełnienia stosu.
Ronnie Overby,
1
Ronnie, myślę, że nie, kod uruchamia rekursję tylko w przypadku, gdy jeszcze jej namenie ma AppDomain.CurrentDomain.GetAssemblies(), co oznacza, że ​​powtórzy się tylko wtedy, gdy foreachwybrany AssemblyNamenie jest jeszcze załadowany.
Felype
1
Nie podoba mi się O(n^2)czas działania tego algorytmu ( GetAssemblies().Any(...)wewnątrz a foreach)). Użyłbym, HashSetżeby sprowadzić to do czegoś w kolejności O(n).
Dai
16

Jeśli korzystasz z Fody.Costura lub innego rozwiązania scalającego montaż, zaakceptowana odpowiedź nie zadziała.

Poniższe czynności ładują zestawy, do których istnieją odniesienia, dowolnego aktualnie załadowanego zestawu. Rekurencja jest pozostawiona Tobie.

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
źródło
Chcesz doradzić, gdzie powinien się znaleźć ten fragment kodu?
Telemat
1
Wyobrażam sobie, że w programie ładującym / rozruchowym.
Meirion Hughes
1
Może się mylę, ale myślę, że możesz po prostu sprawdzić !y.IsDynamicw swoim.Where
Felype
1

Widząc, że musiałem dziś załadować zestaw + zależności z określonej ścieżki, napisałem tę klasę, aby to zrobić.

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
źródło
1
dobry kod, tylko że słownik nie jest potrzebny, w tym przypadku wystarczyłaby prosta lista. Zakładam, że twój oryginalny kod musiał wiedzieć, które zestawy zostały załadowane, a które nie, dlatego masz słownik.
Reinis
0

Jeszcze inna wersja (oparta na odpowiedzi Daniela Schaffera ) to przypadek, w którym może nie być konieczne ładowanie wszystkich zestawów, ale wstępnie zdefiniowana ich liczba:

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
źródło
0

Jeśli masz zestawy, do których nie odwołuje się żaden kod w czasie kompilacji, te zestawy nie zostaną uwzględnione jako odwołanie do innego zestawu, nawet jeśli dodano projekt lub pakiet NuGet jako odwołanie. Jest to niezależnie od tego, Debugczy Releasebudować ustawień, optymalizacja kodu, itp w tych przypadkach, trzeba jawnie wywołać Assembly.LoadFrom(dllFileName)dostać zespół załadowany.

jjxtra
źródło
0

Aby uzyskać odwołanie do zestawu według nazwy, możesz użyć następującej metody:

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
źródło
0

W mojej aplikacji winforms daję JavaScript (w kontrolce WebView2) możliwość wywołania różnych rzeczy .NET, na przykład metod Microsoft.VisualBasic.Interactionw zestawie Microsoft.VisualBasic.dll (takich jak InputBox()itp.).

Ale moja aplikacja jako taka nie używa tego zestawu, więc zestaw nigdy nie jest ładowany.

Aby wymusić załadowanie zestawu, po prostu dodałem to w moim 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
}

Kompilator uważa, że ​​montaż może być potrzebny, ale w rzeczywistości tak się nigdy nie dzieje.

Niezbyt wyszukane rozwiązanie, ale szybkie i brudne.

Magnus
źródło