Создание расширяемых приложений с помощью MAF

В прошлом месяце мой коллега, Пинку Сурана (Pinku Surana), написал статью о .NET AppDomains и о том, как их можно использовать для изоляции компонентов и сделать разрабатываемые приложения более надежными. Мне хотелось бы продолжить изучение надежности и расширяемости, представляя новую платформу, включенную в .NET 3.5: MAF (Managed Add-in Framework, платформа управляемых надстроек), иногда называемая System.AddIn.

Разработчики (и руководители) долго мечтали о простом способе создания расширяемых приложений, позволяющих добавлять новые возможности, не рискуя стабильностью существующей базы кода. Платформа .NET Framework обеспечила поддержку этих действий с самого начала с помощью интерфейса API Reflection и поддержки AppDomain, изученной Пинку в прошлом месяце. MAF опирается на эту фундаментальную поддержку, чтобы предоставить службу высокого уровня, позволяющую динамически обнаруживать внешние сборки, загружать их, обеспечивать их безопасности и взаимодействовать с ними, чтобы предоставить нужные возможности разрабатываемому приложению.

Несколько типовых требований к архитектуре, реализация которых с помощью MAF тривиальна

  1. Изоляция аспектов кода по соображениям безопасности или в сценариях частичного доверия.
  2. Разрешение бизнес-партнерам безопасно расширять приложение без доступа к исходному коду, отличным примером приложений такого типа является Adobe Illustrator.
  3. Выделение переменных разделов приложения, в которых приложение в зависимости от клиента должно выполнять различные фрагменты кода.
  4. Добавление или изменение кода, не выгружая приложение — например, в сценариях "заплати, чтобы играть" или когда нужно заменить сборку, но выполнение приложения должно продолжаться.
  5. Параллельные разработка и развертывание различных разделов приложения, не опасаясь дестабилизации одной задачи из-за другой.

Если какой-то из этих сценариев может быть использован в вашем приложении, то вам следует познакомиться с MAF!

Конвейер

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

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

Для надежности MAF позволяет создавать между надстройкой и основным приложением дополнительные изоляционные границы: все, находящееся слева от контракта, загружается в главный AppDomain (основного приложения), а все, находящееся слева — во вновь создаваемый AppDomain со своим собственным набором разрешений безопасности. Если необходима истинная изоляция на уровне процессов, ее также можно реализовать с помощью межпроцессных вызовов. Внутри система использует традиционные удаленные вызовы для выполнения прямого и обратного преобразования вызовов.

Разбирая конвейер, можно выделить три основные части, центральной из которых является контракт.

Контракт

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

Рассмотрим простой пример программы-преобразователя. Основное приложение будет ожидать загрузки одной или нескольких надстроек преобразования, которые выполняют определенные действия над входящей строкой и возвращают результаты. Для этого я могу создать интерфейс ITranslator, выглядящий следующим образом:

[AddInContract]
public interface ITranslator : IContract
{
    string Translate(string input);
}

Обратите внимание, что интерфейс является производным от IContract. Это обязательно для контрактов надстроек и именно это будет использоваться, чтобы обеспечить поддержку преобразования данных при создании конвейера. Нам также нужно оформить интерфейс, используя атрибут [AddInContract] — это маркер, используемый MAF для определения контракта при динамическом создании конвейера. Оба эти типа находятся в пространстве имен System.AddIn.Pipeline библиотеки System.AddIn.Contract.dll.

Представления

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

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

public abstract class TranslatorHostView
{
    public abstract string Translate(string input);
}

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

На стороне надстройки у нас есть практически идентичный класс — за исключением оформления этого типа с помощью атрибута [AddInBase], чтобы платформе MAF было известно, для какой стороны конвейера предназначено это представление (надстройка). Оно находится в пространстве имен System.AddIn.Pipeline сборки System.AddIn.

При создании каждой надстройки реализация буде использовать этот тип в качестве базового класса.

[AddInBase]
public abstract class TranslatorHostView
{
    public abstract string Translate(stringinp);
}

Адаптеры

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

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

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

[HostAdapter]
public class TranslatorHostViewToContract : TranslatorHostView
{
    ITranslator _contract;
    ContractHandle _lifetime;
 
    public TranslatorHostViewToContract(ITranslator contract)
    {
        _contract = contract;
        _lifetime = new ContractHandle(contract);
    }
 
    public override string Translate (string inp)
    {
        return _contract.Translate(inp);
    }
}

В этом простом примере код кэширует интерфейс контракта. Это посредник удаленного взаимодействия для фактически загруженной надстройки. Затем адаптер реализует метод Translate и передает его контракту для реализации. При использовании несериализуемых типов адаптер будет отвечать за их преобразование в что-то сериализуемое.

Он также предоставляет контракту ряд возможностей управления сроком службы. Так как контракт является посредником удаленного взаимодействия и, скорее всего, работает в отдельном домене AppDomain (или даже процессе), нам придется заботиться о том, как долго он будет существовать. MAF предоставляет всю соответствующую поддержку с помощью класса ContractHandle. Большую часть времени все, что нам понадобится делать — хранить ContractHandle в адаптере основного приложения, а затем передать его входящему контракту для обертывания в конструкторе.

Наконец, чтобы платформа MAF идентифицировала этот класс, он должен быть оформлен атрибутом [HostAdapter] из пространства имен System.AddIn.Pipeline библиотеки System.AddIn.dll.

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

[AddInAdapter]
public class TranslatorAddInViewToContract : ContractBase, ITranslator
{
    TranslatorAddInView _view;
 
    public TranslatorAddInViewToContract(TranslatorView view)
    {
        _view = view;
    }
 
    public string Translate(string inp) 
    {
        return _view.Translate(inp);
    }
}

MAF передает представление конструктору, и класс кэширует ссылку в поле. Он реализует контракт (ITranslator) и класс ContractBase, обеспечивающий для нас реализацию интерфейса IContract (не забывайте, что этот интерфейс был обязательным в нашем контракте). Когда основное приложение осуществляет вызовы интерфейса контракта, этот класс будет преобразовывать эти вызовы в представление надстройки, которое, как вы увидите, является реализацией, предоставляемой самой надстройкой. Обратите внимание, что этот класс помечается атрибутом [AddInAdapter], чтобы платформа MAF могла обнаружить его.

Если кажется, что все вышеописанное — это просто много повторяющегося стандартного кода... ну, вы правы! Чтобы упростить для разработчика создание компонентов конвейера, группа MAF создала построитель конвейера, доступный по адресу http://www.codeplex.com/clraddins. Он получает сборку контракта и создает из нее представления и адаптеры:

Создание надстройки

Создав части конвейера, можно создать надстройку. Каждая надстройка обеспечивает реализацию абстрактного представления надстройки. Например, можно предоставить надстройку BabelFish для универсального преобразования:

[AddIn("BabelFishTranslator", Description="Universal translator",
Version="1.0.0.0", Publisher="Zaphod Beeblebrox")]
public class BableFishAddIn : TranslatorAddInView
{
    public string Translate(string input)
    {
        ...
    }
}

Надстройка реализует AddInView, обеспечивая реализацию метода Translate. Для оформления используется атрибут [AddIn], позволяющий предоставить имя, версию, описание и другие данные, которые основное приложение может использовать, чтобы определить, является ли этот преобразователь полезным.

Объединение компонентов — структура каталогов

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

Имена каталогов являются обязательными, но независящими от регистра — каждый каталог содержит один элемент конвейера, который MAF динамически загрузит при загрузке надстройки. Представление на стороне основного приложения всегда находится в том же каталоге, что и исполняемый файл основного приложения, поэтому ему не потребуется отдельный каталог. При создании проекта Visual Studio важно правильно задать выходные каталоги, чтобы создать вышеописанную структуру каталогов. Кроме того, все ссылки между компонентами должны быть помечены в Visual Studio как CopyLocal = "false", чтобы гарантировать, что локальная копия сборки не будет помещаться в дочерний каталог:

Обнаружение и загрузка надстроек из основного приложения

Последним кусочком головоломки является собственно загрузка надстроек основным приложением. Это выполняется в три основных шага.

  1. Определение и классификация надстройки.
  2. Возвращение списка конкретных надстроек на основе представления или имени.
  3. Активация и использование надстройки.

Первым шагом является определение надстроек, доступных основному приложению. Для этого используется класс System.AddIn.AddInStore:

string[] errorList = AddInStore.Rebuild(
         PipelineStoreLocation.ApplicationBase);

Вызов Rebuild заставляет MAF просмотреть структуру каталогов и создать базу данных конвейера. Эти сведения сохраняются в корневом каталоге конвейера, также возвращается список обнаруженных ошибок. Наиболее распространенными ошибками являются отсутствующие компоненты конвейера — когда MAF не удается обнаружить некоторые обязательные части конвейера, например представление или адаптер.

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

Collection<AddInToken> addInTokens = AddInStore.FindAddIns(
                typeof(TranslatorHostView),
                PipeLineStoreLocation.ApplicationBase);

Первый параметр — это тип представления основного приложения, по нему MAF определяет, поиск каких надстроек ведется, второй — это корневой каталог конвейера, который совпадает с каталогом, переданным в метод Rebuild, и показывает, где хранится база данных конвейера.

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

Collection<AddInToken> addInTokens
    ...
foreach (AddInToken token in addInTokens)
{
    TranslatorHostView view = 
        token.Activate<TranslatorHostView>(
            AddInSecurityLevel.Internet);
 
    string hello = view.Translate("Bonjour");
 
    ...
}

Вызов Activate загружает все необходимые компоненты, создает экземпляр типа надстройки и возвращает представление основного приложения, используемое для взаимодействия с надстройкой. Вызовы, обращенные к этому объекту, будут преобразовываться при прохождении туда-обратно с помощью конвейера.

Обратите внимание, что параметр, переданный в метод Activate, показывает необходимый уровень безопасности. Существует ряд переопределений, позволяющих точно указывать, как загружается надстройка. Надстройку можно загрузить в конкретный домен AppDomain — например текущий для максимального быстродействия:

token.Activate(AppDomain.CurrentDomain)

Можно задать конкретный набор разрешений, чтобы ограничить набор действий, которые можно выполнять приложению от имени пользователя:

PermissionSet pset = ...;
token.Activate<TranslatorHostView>(pset);

Либо можно задать известный набор разрешений, основанный на зонах CAS:

После активации надстройки основное приложение может вызывать ее как любой другой объект — но не забывайте, что при этом пересекается изоляционный барьер! Постарайтесь создавать свои контракты так, чтобы минимизировать количество вызовов между основным приложением и надстройкой, чтобы не пострадало быстродействие.

Когда использование надстройки закончено, можно скомандовать MAF выгрузить ее с помощью объекта типа AddInController, связанного с представлением:

AddInController ctrl =
    AddInController.GetAddInController(view);
ctrl.Shutdown();

При этом будет выгружен конвейер на стороне надстройки, а затем разрушен содержащий надстройку домен AppDomain, освобождая связанные с ним ресурсы.

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