Как предварительно загрузить все развернутые сборки для AppDomain

ОБНОВЛЕНИЕ: теперь у меня есть решение, которое меня гораздо больше устраивает. Хотя я и не решил все проблемы, о которых я спрашиваю, оно оставляет путь для этого свободным. Я обновил свой ответ, чтобы отразить это.

Исходный вопрос

Учитывая домен приложения, существует множество различных мест, которые Fusion (загрузчик сборок .Net) будет исследовать для данной сборки. Очевидно, мы принимаем эту функциональность как должное, и поскольку зондирование, по-видимому, встроено в среду выполнения .Net (внутренний метод Assembly._nLoad, кажется, является точкой входа, когда Reflect-Loading), и я предполагаю, что неявная загрузка, вероятно, покрывается тем же базовый алгоритм), поскольку мы, разработчики, похоже, не можем получить доступ к этим путям поиска.

Моя проблема в том, что у меня есть компонент, который выполняет большое количество разрешений динамических типов и который должен быть в состоянии гарантировать, что все развернутые пользователем сборки для данного домена приложения будут предварительно загружены, прежде чем он начнет свою работу. Да, это замедляет запуск, но преимущества, которые мы получаем от этого компонента, полностью перевешивают это.

Базовый алгоритм загрузки, который я уже написал, выглядит следующим образом. Он глубоко сканирует набор папок на предмет наличия любых .dll (.exes исключаются в данный момент) и использует Assembly.LoadFrom для загрузки dll, если ее AssemblyName не может быть найдено в наборе сборок. уже загружены в AppDomain (это реализовано неэффективно, но может быть оптимизировано позже):

void PreLoad(IEnumerable<string> paths)
{
  foreach(path p in paths)
  {
    PreLoad(p);
  }
}

void PreLoad(string p)
{
  //all try/catch blocks are elided for brevity
  string[] files = null;

  files = Directory.GetFiles(p, "*.dll", SearchOption.AllDirectories);

  AssemblyName a = null;
  foreach (var s in files)
  {
    a = AssemblyName.GetAssemblyName(s);
    if (!AppDomain.CurrentDomain.GetAssemblies().Any(
        assembly => AssemblyName.ReferenceMatchesDefinition(
        assembly.GetName(), a)))
      Assembly.LoadFrom(s);
  }    
}

LoadFrom используется, потому что я обнаружил, что использование Load () может привести к дублированию сборок, загружаемых Fusion, если, когда он исследует их, он не находит загруженную из того места, где он ожидает ее найти.

Итак, с этим все, что мне теперь нужно сделать, это получить список в порядке приоритета (от самого высокого до самого низкого) путей поиска, которые Fusion будет использовать при поиске сборки. Затем я могу просто перебрать их.

GAC для этого не имеет значения, и меня не интересуют какие-либо фиксированные пути, управляемые средой, которые может использовать Fusion - только те пути, которые можно извлечь из AppDomain, которые содержат сборки, специально развернутые для приложения.

Моя первая итерация просто использовала AppDomain.BaseDirectory. Это работает для служб, приложений форм и консольных приложений.

Однако это не работает для веб-сайта Asp.Net, поскольку существует как минимум два основных местоположения - AppDomain.DynamicDirectory (где Asp.Net размещает свои динамически сгенерированные классы страниц и любые сборки, на которые ссылается код страницы Aspx) и затем папка Bin сайта, которую можно найти в свойстве AppDomain.SetupInformation.PrivateBinPath.

Итак, теперь у меня есть рабочий код для большинства основных типов приложений (домены приложений, размещенные на сервере Sql, - это еще одна история, поскольку файловая система виртуализирована), но пару дней назад я столкнулся с интересной проблемой, когда этот код просто не работает : средство запуска тестов nUnit.

При этом используется как теневое копирование (так что моему алгоритму необходимо будет обнаруживать и загружать их из папки сброса теневой копии, а не из папки bin), и он устанавливает PrivateBinPath как относящийся к базовому каталогу.

И, конечно же, есть множество других сценариев хостинга, которые я, вероятно, не рассматривал; но это должно быть действительным, потому что в противном случае Fusion задохнется при загрузке сборок.

Я хочу перестать разбираться и предлагать взлом за взломом, чтобы приспособить эти новые сценарии по мере их появления - я хочу, учитывая домен приложения и его информацию о настройке, возможность создать этот список папок, которые я должен сканировать, чтобы выбрать поднять все библиотеки DLL, которые будут загружены; независимо от того, как настроен домен приложения. Если Fusion видит в них все одинаковые, то и мой код тоже.

Конечно, мне, возможно, придется изменить алгоритм, если .Net изменит свою внутреннюю структуру - это просто крест, который мне придется нести. Точно так же я счастлив рассматривать SQL Server и любые другие подобные среды как крайние случаи, которые пока не поддерживаются.

Любые идеи!?


person Andras Zoltan    schedule 11.06.2010    source источник
comment
не совсем ответ, но мне очень помогла эта статья MSDN: msdn. microsoft.com/en-us/library/yx7xezcf.aspx). И упомянутый Fuslogvw.exe должен помочь понять, почему эта dll не найдена.   -  person ralf.w.    schedule 21.06.2010
comment
ralf.w: Мне было интересно, смогу ли я обмануть, получив журнал слияния для несуществующей сборки и проанализировав его, чтобы увидеть все места, где он ищет. Хотя это, вероятно, сработает, я чувствую, что мне придется включить Fusion Logging в целевом поле (= медленно); плюс тот факт, что я буду кодировать по исключениям, что просто неправильно! Спасибо за ссылку на статью - возможно, немного больше майнинга MSDN может дать мне ответ ...   -  person Andras Zoltan    schedule 21.06.2010
comment
У меня именно такая проблема, не могли бы вы поделиться кодом, который вы получили? Благодарность :)   -  person Andrew Bullock    schedule 20.10.2010
comment
@ Эндрю Баллок: с удовольствием; но я еще не закончил писать и тестировать его! Как только я его получу, я обязательно размещу здесь код. Действительно, если вы заранее получите хорошее решение, не стесняйтесь добавлять здесь ответ, и я отмечу вас как ответ :)   -  person Andras Zoltan    schedule 20.10.2010


Ответы (3)


Теперь я смог найти что-то гораздо более близкое к окончательному решению, за исключением того, что оно все еще неправильно обрабатывает путь к частному бункеру. Я заменил на него свой ранее работающий код, а также исправил несколько неприятных ошибок времени выполнения, которые у меня были (динамическая компиляция кода C # со ссылками на слишком много dll).

С тех пор я обнаружил золотое правило: всегда использовать нагрузку context, а не контекст LoadFrom, поскольку контекст Load всегда будет первым местом, куда .Net смотрит при выполнении естественного связывания. Следовательно, если вы используете контекст LoadFrom, вы получите результат только в том случае, если вы действительно загрузите его из того же места, откуда он, естественно, привязывает его, что не всегда легко.

Это решение работает как для веб-приложений, так и с учетом разницы между папками bin и «стандартными» приложениями. Его можно легко расширить для решения проблемы PrivateBinPath, как только я смогу получить надежную информацию о том, как именно он читается (!)

private static IEnumerable<string> GetBinFolders()
{
  //TODO: The AppDomain.CurrentDomain.BaseDirectory usage is not correct in 
  //some cases. Need to consider PrivateBinPath too
  List<string> toReturn = new List<string>();
  //slightly dirty - needs reference to System.Web.  Could always do it really
  //nasty instead and bind the property by reflection!
  if (HttpContext.Current != null)
  {
    toReturn.Add(HttpRuntime.BinDirectory);
  }
  else
  {
    //TODO: as before, this is where the PBP would be handled.
    toReturn.Add(AppDomain.CurrentDomain.BaseDirectory);
  }

  return toReturn;
}

private static void PreLoadDeployedAssemblies()
{
  foreach(var path in GetBinFolders())
  {
    PreLoadAssembliesFromPath(path);
  }
}

private static void PreLoadAssembliesFromPath(string p)
{
  //S.O. NOTE: ELIDED - ALL EXCEPTION HANDLING FOR BREVITY

  //get all .dll files from the specified path and load the lot
  FileInfo[] files = null;
  //you might not want recursion - handy for localised assemblies 
  //though especially.
  files = new DirectoryInfo(p).GetFiles("*.dll", 
      SearchOption.AllDirectories);

  AssemblyName a = null;
  string s = null;
  foreach (var fi in files)
  {
    s = fi.FullName;
    //now get the name of the assembly you've found, without loading it
    //though (assuming .Net 2+ of course).
    a = AssemblyName.GetAssemblyName(s);
    //sanity check - make sure we don't already have an assembly loaded
    //that, if this assembly name was passed to the loaded, would actually
    //be resolved as that assembly.  Might be unnecessary - but makes me
    //happy :)
    if (!AppDomain.CurrentDomain.GetAssemblies().Any(assembly => 
      AssemblyName.ReferenceMatchesDefinition(a, assembly.GetName())))
    {
      //crucial - USE THE ASSEMBLY NAME.
      //in a web app, this assembly will automatically be bound from the 
      //Asp.Net Temporary folder from where the site actually runs.
      Assembly.Load(a);
    }
  }
}

Сначала у нас есть метод, используемый для получения выбранных нами «папок приложений». Это места, где будут развернуты сборки, развернутые пользователем. Это IEnumerable из-за PrivateBinPath крайнего случая (это может быть ряд местоположений), но на практике в данный момент это только одна папка:

Следующий метод - PreLoadDeployedAssemblies(), который вызывается перед выполнением каких-либо действий (здесь он указан как private static - в моем коде он взят из гораздо более крупного статического класса, имеющего общедоступные конечные точки, которые всегда будут запускать этот код перед выполнением каких-либо действий для первого время.

Наконец, мясо и кости. Здесь самое важное - взять файл сборки и получить его имя сборки, которое затем передать в Assembly.Load(AssemblyName), а не использовать LoadFrom.

Раньше я думал, что LoadFrom более надежен и что вам нужно вручную искать временную папку Asp.Net в веб-приложениях. Вы этого не сделаете. Все, что вам нужно, это знать имя сборки, которая, как вы знаете, обязательно должна быть загружена, и передать его Assembly.Load. В конце концов, это практически то, что делают процедуры загрузки ссылок .Net :)

Точно так же этот подход хорошо работает с настраиваемым зондированием сборки, реализованным путем зависания события AppDomain.AssemblyResolve: расширьте папки bin приложения на любые папки контейнера плагина, которые могут у вас быть, чтобы они были просканированы. Скорее всего, вы уже обработали событие AssemblyResolve, чтобы гарантировать, что они загрузятся, когда обычное зондирование не удастся, поэтому все работает как раньше.

person Andras Zoltan    schedule 15.09.2010
comment
Это круто. Именно то, что я искал. Спасибо, что разместили обновленный код !!!! - person Byron Whitlock; 28.04.2011
comment
выполняете ли вы какую-либо проверку файла DLL перед сборкой Assembly.Load, чтобы проверить, является ли он вообще DLL-файлом .net, или вы просто ловите исключение и двигаетесь дальше? - person JJS; 17.08.2016
comment
@JJS - да, все потенциальные точки разрыва будут в try/catch - в ситуации, когда я его использовал, у нас не было никаких DLL, отличных от CLR, так что это никогда не было проблемой, но все должно быть в порядке. - person Andras Zoltan; 01.09.2016

Вот что я делаю:

public void PreLoad()
{
    this.AssembliesFromApplicationBaseDirectory();
}

void AssembliesFromApplicationBaseDirectory()
{
    string baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
    this.AssembliesFromPath(baseDirectory);

    string privateBinPath = AppDomain.CurrentDomain.SetupInformation.PrivateBinPath;
    if (Directory.Exists(privateBinPath))
        this.AssembliesFromPath(privateBinPath);
}

void AssembliesFromPath(string path)
{
    var assemblyFiles = Directory.GetFiles(path)
        .Where(file => Path.GetExtension(file).Equals(".dll", StringComparison.OrdinalIgnoreCase));

    foreach (var assemblyFile in assemblyFiles)
    {
        // TODO: check it isnt already loaded in the app domain
        Assembly.LoadFrom(assemblyFile);
    }
}
person Andrew Bullock    schedule 02.11.2010
comment
интересно - похоже, хорошее решение; однако папка PrivateBinPath может быть, по-видимому, списком папок, разделенных точкой с запятой в ApplicationBase, из которых будут загружаться библиотеки DLL. Они могут быть относительными или абсолютными (хотя они будут проигнорированы, если не находятся в ApplicationBase). Так что, возможно, здесь нужна доработка. Также - в приложении Asp.Net важно сначала загрузить библиотеки DLL из временного каталога Asp.Net при использовании LoadFrom. Если вы загрузите A.dll из bin \, среда выполнения позже загрузит ее снова из временной папки, и вы получите две копии одной и той же сборки :( - person Andras Zoltan; 03.11.2010
comment
Я использую его в приложении asp.net, и пока у меня не было никаких проблем. мне нужно сначала загрузить из темпа? гррр, это такая дерьмовая проблема. - person Andrew Bullock; 03.11.2010
comment
да, вы делаете это в полной безопасности (похоже, это зависит от того, где среда выполнения будет размещать сборку, если загружена органически, и мне удалось удалить только дублированные сборки таким образом). Отлаживать эту конкретную проблему тоже было головной болью! Впрочем - все равно +1 :) - person Andras Zoltan; 26.11.2010
comment
Я опубликовал свое последнее улучшенное решение. Он немного отличается от моего оригинального и вашего - и я могу поручиться, что он отлично работает (за исключением случаев, когда есть PrivateBinPath на месте, что, я думаю, будет простым исправлением). Он работает в веб-приложениях, обычных приложениях Windows и тестах, размещенных в MS Test. - person Andras Zoltan; 19.01.2011

Вы пробовали смотреть Assembly.GetExecutingAssembly (). Location? Это должно дать вам путь к сборке, из которой выполняется ваш код. В случае с NUnit я ожидал, что это будет то место, куда сборки были скопированы теневым образом.

person Andy    schedule 17.06.2010
comment
Да, это была одна из моих первых попыток сделать это, но это ненадежно в некоторых более сложных ситуациях (например, средство запуска тестов VS). Кроме того, в случае Asp.Net, например, загрузчик сборки фактически имеет множество папок, которые он будет искать, которые находятся за пределами местоположения исполняемой сборки. - person Andras Zoltan; 17.06.2010
comment
Я определенно согласен с тем, что одного этого недостаточно, чтобы охватить все случаи ... Я думал, что это даст вам просто еще одну точку данных. Итак, если я вас правильно понимаю, вы ищете один API, который вы можете вызвать, который предоставит вам все пути поиска Fusions? - person Andy; 18.06.2010
comment
что ж, это наверняка был бы святым Граалем; Я не вижу ничего в инфраструктуре .Net, которая бы это делала, и что касается неуправляемых API; Меня это не пугает (уже много лет прорезал зубы на C ++), но найти что-то сложно. Я посмотрел Fusion API, но он в основном для работы с GAC - person Andras Zoltan; 18.06.2010
comment
Я не согласен с тем, что Assembly.GetExecutingAssembly () ненадежен в случае средства запуска тестов VS (при условии, что вы имеете в виду модульное тестирование Visual Studio). Модульные тесты запускаются из временных каталогов, которые, как Environment.CurrentDirectory во время отладки, не соответствуют окончательному пути вывода. Возможно, все, что вам нужно, это еще немного отладки. Я написал программы, которые требуют, чтобы я вручную копировал мою стороннюю библиотеку DLL как часть моей процедуры [ClassInitialize ()] в указанные временные каталоги, чтобы сборки правильно разрешались во время модульных тестов. - person P.Brian.Mackey; 22.06.2010
comment
@ P.Brian.Mackey: Спасибо за ваш комментарий. В VS Test Runner эта система довольно успешно работает с использованием AppDomain.BaseDirectory, который обычно преобразуется в каталог Out / текущего результата теста. Однако средство запуска тестов nUnit (используемое коллегой) использует теневое копирование и дополнительные относительные пути для PrivateBinPath, поэтому он не работает. Точно так же GetExecutingAssembly в Asp.Net решает только часть проблемы, потому что это будет динамическая dll. создан для текущей страницы; который предоставляет динамический каталог, но не папку bin \, поэтому мне также нужно удвоить использование BaseDirectory. - person Andras Zoltan; 22.06.2010