Есть ли способ принудительно загрузить все сборки, на которые есть ссылки, в домен приложения?


83

Мои проекты устроены так:

  • Проект «Определение»
  • Реализация проекта"
  • Проект «Потребитель»

Проект «Потребитель» ссылается как на «Определение», так и «Реализация», но не ссылается статически ни на какие типы в «Реализация».

Когда приложение запускается, проект «Потребитель» вызывает статический метод в «Определении», которому необходимо найти типы в «Реализации».

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


1
Какие проблемы это вызывает? Зачем нужно форсировать загрузку?
Майк Два

Он вообще не загружается, по-видимому, потому что нет статической зависимости,
Дэниел Шаффер,

Как вы пытаетесь «найти типы» в реализации? Вы ищете что-то, что реализует определенный интерфейс?
Майк Два

2
@ Майк: Да. Я делаю 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 () .. Хм ...
Тед

11
Какие удобства? Поиском ничего не нашел.
nuzzolilo

8
Используя MEF, приведенный выше код можно сократить до: new DirectoryCatalog(".");(требуется ссылка System.ComponentModel.Composition).
Аллон Гуралнек

1
Этот ответ решил мою проблему и сработал для меня. У меня был проект модульного тестирования MS, ссылающийся на другую из моих сборок, и AppDomain.CurrentDomain.GetAssemblies () не возвращал эту сборку при запуске теста. Я подозреваю, что, хотя в моих модульных тестах использовался код из этой библиотеки, сборка могла не отображаться в "GetAssemblies" из-за того, как vs.net загружает проект модульного теста MS (библиотеку классов) по сравнению с запуском обычного .exe приложение. О чем следует помнить, если в вашем коде используется отражение и не удается выполнить модульные тесты.
Dean Lunz

4
Просто хотел добавить, будьте осторожны с динамически загружаемыми сборками. Вызванный член не поддерживается в динамической сборке. Либо отфильтруйте сборки, где IsDynamic = false, либо, если вы можете быть отказоустойчивым к нагрузкам, попробуйте / перехватите свой вызов CurrentDomain.Load. И assembly.Location. Этот тоже нужно проверить. Не работает для IsDynamicсборок.
Эли Гассерт,

63

Вы можете использовать, Assembly.GetReferencedAssembliesчтобы получить AssemblyName[], а затем позвонить Assembly.Load(AssemblyName)каждому из них. Конечно, вам нужно будет выполнить рекурсию, но желательно отслеживать сборки, которые вы уже загрузили :)


Я обнаружил это, но проблема в том, что я должен делать все, что я делаю, из указанной сборки ... и, по крайней мере, в контексте модульного теста GetCallingAssembly, GetExecutingAssembly, конечно, возвращают указанную сборку, а GetEntryAssembly возвращает null : \
Daniel Schaffer

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

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еще не загружен.
Felype

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

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
Felype

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.