Как предварительно загрузить все развернутые сборки для домена приложений - PullRequest
29 голосов
/ 11 июня 2010

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

Оригинальный вопрос

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

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

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

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 Server - еще одна история, поскольку файловая система виртуализирована) - но я столкнулся с интересной проблемой пару дней назад, когда этот код просто не не работает: тестовый блок nUnit.

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

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

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

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

Есть идеи!?

Ответы [ 3 ]

19 голосов
/ 16 сентября 2010

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

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

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

4 голосов
/ 02 ноября 2010

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

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);
    }
}
0 голосов
/ 17 июня 2010

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

...