Чи є спосіб примусити всі збірки, на які посилаються, завантажуватись у домен програми?


83

Мої проекти налаштовані так:

  • Проект "Визначення"
  • Проект "Впровадження"
  • Проект "Споживач"

Проект "Споживач" посилається як на "Визначення", так і на "Впровадження", але статично не посилається на жоден тип у "Впровадженні".

Коли програма запускається, проект "Споживач" викликає статичний метод у "Визначенні", який повинен знаходити типи в "Впровадженні"

Чи я можу змусити будь-яку згадану збірку завантажувати в домен програми, не знаючи шляху або імені, і бажано без використання повноцінного фреймворка IOC?


1
Яку проблему це викликає? Навіщо потрібно форсувати навантаження?
Mike Two

Він взагалі не завантажується, мабуть, тому, що немає статичної залежності
Даніель Шаффер

Як ви намагаєтесь "знайти типи" у реалізації? Ви шукаєте щось, що реалізує певний інтерфейс?
Mike Two

2
@Mike: Так. Я роблю AppDomain.CurrentDomain.GetAssemblies і використовую запит linq для рекурсивного виклику GetTypes () на кожному з них.
Даніель Шаффер,

Відповіді:


90

Здавалося, це зробило фокус:

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

Як зазначив Джон, ідеальне рішення мало б повернутися до залежностей для кожної із завантажених збірок, але в моєму конкретному сценарії мені не доведеться про це турбуватися.


Оновлення: Менеджер керованих розширень (System.ComponentModel), що входить до .NET 4, має набагато кращі можливості для виконання подібних дій.


5
Це не працює для мене, мої посилальні збірки, які хіба завантажені, не відображається в AppDomain.CurrentDomain.GetAssemblies () .. Хм ...
Ted

11
Які зручності? Я нічого не знайшов за допомогою пошуку.
nuzzolilo

8
Використовуючи MEF, наведений вище код можна скоротити до: new DirectoryCatalog(".");(вимагає посилання System.ComponentModel.Composition).
Аллон Гуралнек,

1
Ця відповідь вирішила мою проблему і працювала для мене. У мене був проект модульного тестування MS, який посилався на ще одну мою збірку, і AppDomain.CurrentDomain.GetAssemblies () не повертав цю збірку під час запуску тесту. Я підозрюю, що навіть незважаючи на те, що мої модульні тести використовували код із цієї бібліотеки, збірка, можливо, не відображалася в "GetAssemblies" через те, як vs.net завантажує проект модульного тестування MS (бібліотека класів) порівняно із запуском звичайної .exe. Щось, про що слід пам’ятати, якщо ваш код використовує відображення та не проходить модульні тести.
Дін Лунц,

4
Просто хотів додати, будьте обережні з динамічно завантаженими збірками Викликаний член не підтримується в динамічній збірці. Або фільтруйте збірки, де IsDynamic = false, або якщо ви можете перешкодити навантаженням, спробуйте / перейміть ваш дзвінок до CurrentDomain.Load. І assembly.Location. Це теж потрібно перевірити. Не працює для IsDynamicзбірок.
Елі Гассерт

63

Ви можете використовувати, Assembly.GetReferencedAssembliesщоб отримати AssemblyName[], а потім зателефонувати Assembly.Load(AssemblyName)кожному з них. Звичайно, вам доведеться повторити, але бажано відстежувати вже завантажені збірки :)


Я виявив це, але проблема полягає в тому, що я повинен робити все, що я роблю із згаданої збірки ... і принаймні в контексті модульного тесту GetCallingAssembly, GetExecutingAssembly, звичайно, повертає згадану збірку, а GetEntryAssembly повертає null : \
Даніель Шаффер

4
Якщо ви завантажуєте еталонні збірки, то вищезазначене вирішить вашу проблему. Ви також можете запитати певний тип typeof (T). Зберіть, якщо це допомагає. У мене таке відчуття, що вам потрібно динамічно завантажувати збірки, що містять реалізацію (без посилань). Якщо це так, вам доведеться або зберегти статичний список імен і завантажувати їх вручну, або переглядаючи весь каталог, завантажувати, а потім знаходити тип із потрібними інтерфейсами.
Фадріан Судаман,

1
@vanhelgen: На моєму досвіді це рідко щось, що потрібно чітко сказати. Зазвичай "навантаження на запит" CLR працює нормально.
Джон Скіт,

2
За звичайних обставин це може бути правдою, але при використанні DI-контейнера для виявлення доступних служб (через System.Reflection) він, природно, не знаходить служб, що містяться в збірках, які ще не завантажені. З тих пір моїм підходом за замовчуванням було створення фіктивного підкласу з випадкового типу кожної згаданої збірки в CompositionRoot мого додатка, щоб переконатися, що всі залежності на місці. Я сподіваюся, що зможу пропустити цю нісенітницю, завантаживши все заздалегідь, навіть ціною подальшого збільшення часу запуску. @JonSkeet чи є інший спосіб це зробити? thx
mfeineis

12
Де це падає, так це те, що GetReferencedAssemblies, очевидно, повертає "оптимізований" список - тому, якщо ви явно не викликаєте код у збірці, на яку посилаються, він не буде включений. (За цією дискусією )
FTWinston

23

просто хотів поділитися рекурсивним прикладом. Я викликаю метод LoadReferencedAssembly у моїй процедурі запуску так:

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

Це рекурсивний метод:

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

5
Цікаво, чи посилання на кругові збірки можуть спричинити викиди переповнення стека.
Ронні Овербі

1
Ронні, я вважаю, що ні, код запускає рекурсію лише в тому випадку, якщо її nameще немає AppDomain.CurrentDomain.GetAssemblies(), це означає, що вона повториться, лише якщо foreachвибране AssemblyNameще не завантажено.
Феліпе

1
Я не задоволений O(n^2)роботою цього алгоритму ( GetAssemblies().Any(...)всередині a foreach)). Я б використав a, HashSetщоб звести це до чогось на порядок O(n).
Dai

16

Якщо ви використовуєте Fody.Costura або будь-яке інше рішення для злиття збірок, прийнята відповідь не буде працювати.

Далі завантажується посилання на збірки будь-якої завантаженої на даний момент збірки. Рекурсія залишається за вами.

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

Хочете порадити, куди повинен потрапити цей фрагмент?
Telemat

1
у вашому завантажувачі / запуску, я думаю.
Meirion Hughes

1
Можливо, я помиляюся, але я думаю, що ви можете просто перевірити !y.IsDynamicсвій.Where
Феліпе

1

Побачивши, що сьогодні мені довелося завантажувати збірку + залежності з певного шляху, я написав цей клас, щоб це зробити.

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

1
хороший код, за винятком того, що в Словнику немає потреби, у цьому випадку буде достатньо простого списку. Я припускаю, що ваш оригінальний код мав знати, які збірки завантажувались, а які ні, тому у вас є Словник.
Рейніс,

0

Ще одна версія (на основі відповіді Даніеля Шаффера ) - це той випадок, коли вам може знадобитися не завантажувати всі Асамблеї, а заздалегідь визначену їх кількість:

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

0

Якщо у вас є збірки, на які під час компіляції не посилається жоден код, ці збірки не будуть включені як посилання на вашу іншу збірку, навіть якщо ви додали проект або пакет nuget як посилання. Це незалежно від налаштувань Debugабо Releaseзбірки, оптимізації коду тощо. У цих випадках вам потрібно явно зателефонувати, Assembly.LoadFrom(dllFileName)щоб завантажити збірку.


0

Для отримання посилання на збірку за іменем ви можете використовувати наступний метод:

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

0

У моїй програмі winforms я надаю JavaScript (в елементі керування WebView2) можливість викликати різні .NET-речі, наприклад методи Microsoft.VisualBasic.Interactionзбірки Microsoft.VisualBasic.dll (наприклад, InputBox()тощо).

Але моя програма як така не використовує цю збірку, тому збірка ніколи не завантажується.

Тож, щоб змусити завантажувати збірку, я в кінцевому підсумку просто додав це у свій 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
}

Компілятор вважає, що може знадобитися збірка, але насправді цього, звичайно, ніколи не відбувається.

Не дуже вишукане рішення, але швидке та брудне.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.