Подтвердить что ты не робот

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

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

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

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

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

Основной алгоритм загрузки, который я уже написал, выглядит следующим образом. Он глубоко сканирует набор папок для любой DLL (в настоящее время исключаются исключения .exe) и использует 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 Server-based AppDomains - это еще одна история с момента виртуализации файловой системы), но пару дней назад я столкнулся с интересной проблемой, когда этот код просто не работает 't work: nNnit test runner.

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

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

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

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

Любые идеи!?

4b9b3361

Ответ 1

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

Золотое правило, которое я с тех пор обнаружил, всегда использует контекст нагрузки, а не контекст 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

Это то, что я делаю:

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

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