Bezdepbonusu7Программирование: статьи и ответы

Вопрос

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

Автор вопроса: Andras ZoltanОтветов: 3

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

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

Учитывая домен приложения, существует множество различных мест, которые 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 и любые другие подобные среды как крайние случаи, которые пока не поддерживаются.

Любые идеи!?

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

reflectionassemblies

Ответы

Ответ 1 · рейтинг 19 · Andras Zoltan

Теперь я смог найти что-то гораздо более близкое к окончательному решению, за исключением того, что оно все еще неправильно обрабатывает путь к частному бункеру. Я заменил на него свой ранее работающий код, а также исправил несколько неприятных ошибок времени выполнения, которые у меня были (динамическая компиляция кода 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, чтобы гарантировать, что они загрузятся, когда обычное зондирование не удастся, поэтому все работает как раньше.

Ответ 2 · рейтинг 4 · Andrew Bullock

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

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

Ответ 3 · Andy

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

Перевод вопроса и ответов сообщества Stack Overflow, лицензия CC BY-SA. Оригинал на Stack Overflow.

Похожие материалы