Рекомендации для загрузки сборок

В этой статье рассматриваются способы избежания проблем с идентификацией типов, способных привести к исключениям InvalidCastException, MissingMethodException и другим ошибкам. В статье рассматриваются следующие рекомендации:

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

Понимание преимуществ и недостатков контекстов загрузки

В рамках домена приложения сборки могут загружаться в одном из трех контекстов либо могут быть загружены без контекста:

  • Контекст загрузки по умолчанию содержит сборки, обнаруженные в результате поиска в глобальном кэше сборок, в хранилище сборок главного узла, если среда выполнения является размещенной (например, в SQL Server), либо в свойствах ApplicationBase и PrivateBinPath домена приложения. В большинстве перегруженных версий метода Load сборки загружаются именно в этот контекст.

  • Контекст, из которого ведется загрузка, содержит сборки из расположений, в которых загрузчик не ведет поиск. Например, надстройки могут устанавливаться в каталоге, не относящемся к пути приложения. К примерам методов, которые выполняют загрузку по указанному пути, относятся методы Assembly.LoadFrom, AppDomain.CreateInstanceFrom и AppDomain.ExecuteAssembly.

  • Контекст только для отражения содержит сборки, загружаемые с помощью методов ReflectionOnlyLoad и ReflectionOnlyLoadFrom. Код, загруженный в этом контексте, не может быть выполнен, поэтому далее он не рассматривается. Дополнительные сведения см. в разделе Практическое руководство. Загрузка сборок в контекст, предназначенный только для отражения.

  • В случае создания временной динамической сборки с помощью эмиссии отражения сборка не попадет ни в один контекст. Кроме того, большинство сборок, загружаемых с помощью метода LoadFile, загружаются без контекста, а сборки, загружаемые из байтовых массивов, также загружаются без контекста, если только их идентификаторы (после применения политики) не указывают на то, что они расположены в глобальном кэше сборок.

Контексты выполнения имеют преимущества и недостатки, описанные в следующих разделах.

Контекст загрузки по умолчанию

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

Использование контекста загрузки по умолчанию имеет следующие недостатки:

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

  • В контексте загрузки по умолчанию нельзя загружать сборки из расположений, не относящихся к пути поиска сборок.

Контекст, из которого ведется загрузка

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

Загрузка сборок с помощью метода Assembly.LoadFrom или другого метода, использующего для загрузки путь, имеет следующие недостатки:

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

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

  • Если сборка загружена с использованием метода LoadFrom, но путь поиска сборок включает сборку с тем же идентификатором, находящуюся в другом расположении, то может возникнуть исключение InvalidCastException, MissingMethodException или другое непредусмотренное поведение.

  • Метод LoadFrom требует наличия флагов FileIOPermissionAccess.Read или FileIOPermissionAccess.PathDiscovery либо наличия объекта WebPermission для заданного пути.

  • Если для сборки существует машинный образ, то он не используется.

  • Сборка не может быть загружена как доменно-нейтральная.

  • На платформе .NET Framework версий 1.0 и 1.1 политика не применяется.

Загрузка без контекста

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

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

Загрузка сборок без контекста имеет следующие недостатки:

  • Другие сборки не могут привязываться к сборкам, загруженным без контекста, если только приложение не обрабатывает событие AppDomain.AssemblyResolve.

  • Зависимости не загружаются автоматически. Их можно предварительно загрузить без контекста, загрузить в контексте загрузки по умолчанию или загрузить их в обработчике событий AppDomain.AssemblyResolve.

  • Загрузка нескольких сборок с одним и тем же идентификатором без контекста может привести к проблемам с идентификацией типов, аналогичным тем, что возникают при загрузке сборок с одним и тем же идентификатором в нескольких контекстах. См. раздел Избежание загрузки сборок в нескольких контекстах.

  • Если для сборки существует машинный образ, то он не используется.

  • Сборка не может быть загружена как доменно-нейтральная.

  • На платформе .NET Framework версий 1.0 и 1.1 политика не применяется.

Избежание привязки к частичным именам сборок

Частичная привязка имен происходит при указании лишь одной из частей отображаемого имени сборки (FullName) при ее загрузке. Например, метод Assembly.Load можно вызвать с простым именем сборки, не указывая ее версию, язык и региональные параметры, а также маркер открытого ключа. Также можно вызвать метод Assembly.LoadWithPartialName, который сначала вызывает метод Assembly.Load, а затем, если ему не удалось найти сборку, проводит поиск по глобальному кэшу сборок и загружает последнюю версию сборки из доступных.

Частичная привязка имен может вызывать самые разнообразные проблемы, включая следующие:

  • Метод Assembly.LoadWithPartialName может загрузить другую сборку с тем же простым именем. Например, два приложения могут установить в глобальном кэше сборок две разные сборки с простым именем GraphicsLibrary.

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

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

  • Установка новых приложений может нарушать работу уже имеющихся приложений. Работа приложения, использующего метод LoadWithPartialName, может быть нарушена в результате установки новой несовместимой версии совместно используемой сборки.

  • При загрузке необходимых сборок может возникать непредвиденное поведение. Загрузка двух сборок, совместно зависящих от одной сборки, с использованием частичной привязки имен может привести к тому, что одна из сборок будет использовать компонент, с которым она не строилась и не тестировалась.

Ввиду потенциальных проблем метод LoadWithPartialName был помечен как устаревший. Вместо него рекомендуется использовать метод Assembly.Load, указывая полные отображаемые имена сборок. См. разделы Понимание преимуществ и недостатков контекстов загрузки и Рассмотрение возможности перехода в контекст загрузки по умолчанию.

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

Избежание загрузки сборок в нескольких контекстах

Загрузка сборок в нескольких контекстах может вызывать проблемы с идентификацией типов. Загрузка одного и того же типа из одной и той же сборки в двух разных контекстах аналогична загрузке двух разных типов с одним и тем же именем. При попытке приведения из одного такого типа в другой возникнет исключение InvalidCastException, а в сообщении об ошибке при этом будет несколько туманно сказано, что тип MyType нельзя привести к типу MyType.

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

Теперь подумаем о том, что произойдет при запуске программы. Сборки, на которые ссылается программа, будут загружены в контексте загрузки по умолчанию. Если загрузить целевую сборку по идентификатору с помощью метода Load, то она попадет в контекст загрузки по умолчанию вместе со сборками, от которых она зависит. Как программа, так и целевая сборка будут использовать сборку Utility.

Предположим теперь, что целевая сборка загружается по файловому пути с помощью метода LoadFile. В этом случае сборка загружается без контекста, поэтому необходимые для нее сборки не будут загружены автоматически. Можно создать обработчик событий AppDomain.AssemblyResolve, разрешающий подобные зависимости; он может загрузить сборку Utility без контекста с помощью метода LoadFile. В этом случае при создании экземпляра типа, содержащегося в целевой сборке, и попытке его присвоения переменной типа ICommunicate будет порождено исключение InvalidCastException, так как среда выполнения рассматривает интерфейсы ICommunicate в двух копиях сборки Utility как разные типы.

Существует множество других сценариев, в которых сборка может загружаться в нескольких контекстах. Оптимальный подход состоит в устранении потенциальных конфликтов путем перемещения целевой сборки в путь приложения и использования метода Load с полным отображаемым именем. При этом сборка будет загружаться в контексте загрузки по умолчанию, и обе сборки будут использовать одну и ту же копию сборки Utility.

Если целевая сборка должна находиться вне пути приложения, то можно использовать метод LoadFrom для ее загрузки в контексте, из которого ведется загрузка. Если целевая сборка компилировалась со ссылкой на сборку Utility приложения, то она будет использовать сборку Utility, которую приложение загрузило в контексте загрузки по умолчанию. Обратите внимание, что в случае, если целевая сборка зависит от копии сборки Utility, расположенной вне пути приложения, могут возникнуть проблемы. Если эта сборка загружается в контекст, из которого ведется загрузка, перед тем, как приложение загрузит сборку Utility, загрузка приложения завершится ошибкой.

В разделе Рассмотрение возможности перехода к использованию контекста загрузки по умолчанию описаны альтернативы использованию загрузки по файловому пути, например LoadFile и LoadFrom.

Избежание загрузки нескольких версий сборки в том же контексте

Загрузка нескольких версий сборки в одном контексте загрузки может вызвать проблемы с идентификацией типов. Загрузка одного и того же типа из двух версий одной и той же сборки аналогична загрузке двух разных типов с одним и тем же именем. При попытке приведения из одного такого типа в другой возникнет исключение InvalidCastException, а в сообщении об ошибке при этом будет несколько туманно сказано, что тип MyType нельзя привести к типу MyType.

Предположим, программа загружает одну версию сборки Utility напрямую, а потом загружает другую версию сборки Utility. Приложение также может загрузить две разные версии сборки, если в коде приложения имеется ошибка.

В контексте загрузки по умолчанию эта проблема может возникать при использовании метода Assembly.Load с указанием полных отображаемых имен сборок, включающих разные версии. В случае загрузки сборок вне контекста проблема может возникать, если метод Assembly.LoadFile используется для загрузки одной и той же сборки по нескольким путям. Среда выполнения считает две сборки, загруженные из разных путей, разными, даже если они имеют одинаковые идентификаторы.

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

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

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

Возможность перехода к использованию контекста загрузки по умолчанию

Рассмотрите способы загрузки и развертывания сборок в вашем приложении. Можно ли устранить сборки, загружаемые из байтовых массивов? Можно ли перенести сборки в путь поиска сборок? Если сборки загружаются из глобального кэша сборок или пути поиска сборок домена приложения (то есть ApplicationBase и PrivateBinPath), то сборку можно загрузить по идентификатору.

Если перенести все сборки в путь поиска сборок невозможно, рассмотрите альтернативы: использование модели надстроек платформы .NET Framework, размещение сборок в глобальном кэше сборок или создание доменов приложения.

Использование модели надстроек платформы .NET Framework

При использовании контекста, из которого ведется загрузка, для реализации надстроек, которые обычно устанавливаются вне базовой папки приложения, используйте модель надстроек платформы .NET Framework. Она обеспечивает изоляцию на уровне доменов приложений и процессов, не требуя самостоятельно управлять доменами приложений. Сведения о модели надстроек см. в разделе Надстройки и расширения среды.

Использование глобального кэша сборок

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

Использование доменов приложений

Если выяснится, что некоторые сборки нельзя разместить в пути поиска сборок приложения, подумайте, нельзя ли создать для них отдельный домен приложения. Используйте тип AppDomainSetup для создания нового домена приложения и укажите с помощью свойства AppDomainSetup.ApplicationBase путь, по которому расположены сборки, которые требуется загрузить. При наличии нескольких таких каталогов можно задать в ApplicationBase корневой каталог и использовать свойство AppDomainSetup.PrivateBinPath для указания подкаталогов, в которых нужно вести поиск. Также можно создать несколько доменов приложения и задать в свойстве ApplicationBase каждого из них необходимый путь для соответствующих сборок.

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

См. также