Сведения о System.Runtime.Loader.AssemblyLoadContext

Класс AssemblyLoadContext был представлен в .NET Core и недоступен в .NET Framework. Эта статья служит дополнением к документации по API AssemblyLoadContext, предоставляя сведения об используемых концепциях.

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

Что такое AssemblyLoadContext?

Каждое приложение .NET 5+ и .NET Core неявным образом использует AssemblyLoadContext. Это поставщик среды выполнения для поиска и загрузки зависимостей. При каждой загрузке зависимости вызывается экземпляр AssemblyLoadContext для ее поиска.

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

Правила управления версиями

Один AssemblyLoadContext экземпляр ограничен загрузкой ровно одной версии одного простого Assembly имени сборки. При разрешении ссылки на сборку для AssemblyLoadContext экземпляра, который уже имеет сборку этого имени, запрошенная версия сравнивается с загруженной версией. Разрешение будет выполнено успешно, только если загруженная версия равна или выше запрошенной версии.

Зачем может потребоваться несколько экземпляров AssemblyLoadContext?

Ограничение, которое один AssemblyLoadContext экземпляр может загрузить только одну версию сборки, может стать проблемой при динамической загрузке модулей кода. Каждый модуль компилируется независимо, и модули могут зависеть от разных версий модуля Assembly. Эта проблема часто возникает, когда разные модули зависят от разных версий часто используемой библиотеки.

Для поддержки динамической загрузки кода API AssemblyLoadContext предоставляет возможность загружать конфликтующие версии Assembly в одном приложении. Каждый AssemblyLoadContext экземпляр предоставляет уникальный словарь, который сопоставляет каждый AssemblyName.Name экземпляр с конкретным Assembly экземпляром.

Он также поддерживает удобный механизм группирования зависимостей, связанных с модулем кода, для последующей выгрузки.

Экземпляр AssemblyLoadContext.Default

Среда выполнения автоматически заполняет экземпляр AssemblyLoadContext.Default при запуске. Он использует стандартное зондирование для поиска и определения всех статических зависимостей.

Он реализует все самые распространенные сценарии загрузки зависимостей.

Динамические зависимости

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

Экземпляр AssemblyLoadContext.Default поддерживает переопределение только для событий.

Все доступные события и виртуальные функции описаны в статьях Алгоритм загрузки управляемых сборок, Алгоритм загрузки вспомогательных сборок и Алгоритм загрузки неуправляемых (собственных) библиотек. В тех статьях указаны относительные положения всех событий и функций в алгоритмах загрузки. Мы не будем дублировать эту информацию в этой статье.

В этом разделе описаны общие принципы работы с соответствующими событиями и функциями.

  • Сохраняйте повторяемость. Запрос к определенной зависимости должен всегда возвращать одинаковый ответ. Должен возвращаться один и тот же экземпляр загруженной зависимости. Это требование крайне важно для согласованности кэша. В частности, для управляемых сборок мы создаем кэш Assembly. Ключом этого кэша служит простое имя сборки (AssemblyName.Name).
  • Старайтесь не создавать исключений. Ожидается, что эти функции будут возвращать null, а не создавать исключения при отсутствии запрошенной зависимости. Исключения приводят к преждевременному завершению поиска и передаются вызывающему объекту. Исключения следует применять только для непредвиденных ошибок, например при повреждении сборки или нехватке памяти.
  • Избегайте рекурсии. Имейте в виду, что эти функции и обработчики реализуют правила загрузки для поиска зависимостей. Ваша реализация не должна вызывать API, так как это может привести к рекурсивному поиску. Обычно в этом коде следует вызывать функции загрузки из AssemblyLoadContext, которые ожидают аргумент с определенным путем или ссылкой на память.
  • Загружайте в правильный экземпляр AssemblyLoadContext. Выбор мест для загрузки зависимостей зависит от конкретного приложения. Такой выбор определяется именно этими событиями и функциями. Когда ваш код вызывает функции загрузки по пути из AssemblyLoadContext, их нужно вызывать именно из того экземпляра, в который вы намерены загрузить код. Иногда проще всего будет вернуть значение null и оставить обработку загрузки на усмотрение AssemblyLoadContext.Default.
  • Помните о проблемах состязания потоков. Загрузка может инициироваться в нескольких потоках. AssemblyLoadContext избегает состязания потоков, атомарно добавляя сборку в свой кэш. Экземпляр проигравшего процесса просто отклоняется. Не добавляйте в вашу реализацию дополнительную логику, которая не поддерживает правильную работу с несколькими потоками.

Как изолируются динамические зависимости?

Каждый экземпляр AssemblyLoadContext представляет уникальную область для экземпляров Assembly и определений Type.

Между этими зависимостями отсутствует двоичная изоляция. Они изолируются только тем, что не могут найти друга по имени.

Соответственно, в каждом AssemblyLoadContext:

  • AssemblyName.Name может ссылаться на другой экземпляр Assembly;
  • Type.GetType может возвращать другой экземпляр типа для того же типа name.

Общие зависимости

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

Такой режим совместного использования обязателен для сборок среды выполнения. Такие сборки можно загружать только в AssemblyLoadContext.Default. Это же требование применимо к таким платформам, как ASP.NET, WPF или WinForms.

Мы рекомендуем загружать общие зависимости в AssemblyLoadContext.Default. Совместное использование — широко распространенный конструктивный шаблон.

Совместное использование реализуется в коде пользовательского экземпляра AssemblyLoadContext. AssemblyLoadContext определяет несколько событий и виртуальных функций, которые можно переопределить. Когда любая из этих функций возвращает ссылку на экземпляр Assembly, который был загружен в другом экземпляре AssemblyLoadContext, такой экземпляр Assembly становится общим. Стандартный алгоритм загрузки оставляет загрузку на усмотрение AssemblyLoadContext.Default, чтобы упростить шаблон совместного доступа. Дополнительные сведения см. в статье Алгоритм загрузки управляемых сборок.

Проблемы преобразования типов

Если два экземпляра AssemblyLoadContext содержат определения типов с одинаковыми name, они считаются разными типами. Тип будет считаться одним и тем же только в том случае, если определения поступают из одного экземпляра Assembly.

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

Невозможно преобразовать объект типа "IsolatedType" в тип "IsolatedType".

Проблемы с преобразованием типов отладки

При несовпадении двух типов важно получить следующие сведения:

При наличии двух объектов a и b будет полезно оценить в отладчике следующие данные:

// In debugger look at each assembly's instance, Location, and FullName
a.GetType().Assembly
b.GetType().Assembly
// In debugger look at each AssemblyLoadContext's instance and name
System.Runtime.Loader.AssemblyLoadContext.GetLoadContext(a.GetType().Assembly)
System.Runtime.Loader.AssemblyLoadContext.GetLoadContext(b.GetType().Assembly)

Устранение проблем преобразования типов

Для решения таких проблем с преобразованием типов существует два конструктивных шаблона.

  1. Используйте общие стандартные типы. Такой общий тип может быть примитивным типом среды выполнения или включать создание нового общего типа в общей сборке. Часто общий тип является интерфейсом, определенным в сборке приложения. Дополнительные сведения см. в разделе о том, как реализовано совместное использование зависимостей.

  2. Используйте методы маршаллинга для преобразования из одного типа в другой.