Меня всегда интересовала разработка многоразового и модульного кода. Но проблема многоразового кода останавливается на этапе переноса в другую инфраструктуру. Если приложение расширяется плагинами, то плагины пишутся под конкретное приложение. В этот самый момент мне и пришла идея вывести логику приложения в плагин (далее - модуль), а интерфейс приложения из управляющего звена превратить в управляемый модулем компонент. На мой взгляд, самая главная задача в подобном сценарии, упростить базовые интерфейсы до минимума и дать возможность переписать или расширить любой фрагмент всей инфраструктуры в отдельности. Если интересно что вышло из идеи многоразового кода, то добро пожаловать под кат.
Первое и самое главное условие к предстоящей системе - возможность динамически расширять систему без необходимости рекомпиляции отделных модулей.
Любое звено решения (кроме базовых интерфейсов) может быть переписано и динамически интегрировано. В довесок к возможности расширения модулей интерфейсами, хотелось иметь возможность получать доступ в динамике к публичным методам, свойствам и событиям, которые доступны в любом модуле. Соответственно, все элементы класса реализующего базовый интерфейс IPlugin, которые помечены доступностью как public, должны быть видимы извне другими модулями.
Любой модуль может изыматься и добавляться в систему, но при этом, при решении заменить один модуль другим модулем, придётся реализовывать весь функционал удаляемого модуля. Т.е. Модули идентифицируются через атрибут AssemblyGuidAttribute, добавляемый автоматом при создании сборки.
Каждый модуль должен быть легковестным, чтобы базовые интерфейсы не нуждались в постоянном обновлении, а при необходимости, модуль можно было изять из системы и встроить как обычную сборку в приложение через ссылку (Reference). Благо, CLR не загружает все зависящие сборки, так что нужда в сборках модульной инфраструктуры отпадает.
И последнее условие, система должна предоставлять поэтапное расширение функционала для разработчика, чтобы уровень вхождения был на достаточно низком уровне.
При этом, система должна автоматизировать рутинные задачи, которые повторяются от приложения к приложению. А именно:
В результате накопившихся решений и отдельных компонентов, работающих по единому принципу, было составлено общее видение всей инфраструктуры:
Для предоставления независимости разработки как от конкретного приложения, так и самими программами, появились следующие ключевые компоненты:
В результате этих требований сформировались следующие базовые сборки:
Для минимального функционирования системы, достаточно добавить ссылку на SAL.Core, а при необходимости реализовать или использовать расширения, добавить ссылку на соответствующий набор расширений интерфейсов. Либо самостоятельно расширить минимальный набор интерфейсов нужной абстракцией.
Во время запуска хоста, первым делом инициализируются встроенные в хост базовые модули, для загрузки настроек и внешних плагинов (LoaderProvider и SettingsProvider).
Сначала инициализируется провайдер плагинов, а затем провайдер настроек. Встроеный в хост загрузчик ищет все плагины в папке приложения и подписывается на событие поиска зависимых сборок. Затем, встроенный в хост провайдер настроек, подгружает настройки из XML файла, находящегося в профиле пользователя. Оба провайдера поддерживают иерархическую инфраструктуру наследования и при обнаружении очередного провайдера становятся родителями нового провайдера. Если провайдер не находит требуемые ресурсы, то запрос ресурсов адресуется родительскому провайдеру.
После завершения процесса инициализации всех провайдеров, происходит инициализация всех Kernel, а затем и оставшихся плагинов. В отличие от остальных модулей, Kernel плагины инициализируются в первую очередь, получая возможность подписаться на события загрузки остальных плагинов с возможностью отмены загрузки лишних плагинов.
Данное поведение может быть переписано в хостах, если необходимо соблюсти иерархию загрузки других типов плагинов. Сейчас думаю о выносе последовательности загрузки модулей в Kernel.
Стандартные LoaderProvider через рефлексию ищут все public классы, которые реализуют IPlugin и это не правильный подход. Дело в том, что если в коде идёт вызов конкретного класса или через рефлексию идёт обращение к конкретному классу, и этот класс не ссылается ни на какие сторонние сборки, то события AssemblyResolve не произойдёт. Т.е., сборку можно изъять из модульной инфраструктуры и использовать как обычную сборку добавив на неё ссылку и необходимость в SAL.dll отпадёт. Но текущие провайдеры модулей, реализованы по принципу сканирования всех объектов сборки, поэтому событие AssemblyResolve на все ссылающиеся сборки произойдёт на момент загрузки модуля.
В довесок, при написании сборок на Managed C++, не будет работать подгрузка без физического наличия сборки на файловой системе (из памяти подгрузить не получится). Это связано с невозможностью работы WinAPI функций LoadLibrary / FreeLibrary.
Базовые интерфейсы и небольшие куски кода, реализуемые в абстрактных классах для упрощения разработки. В качестве самой минимальой версии фреймворка для основы, была выбрана версия .NET Framework v2.0. Выбор минимальной необходимой версии позволяет использовать базу на любых платформах поддерживающих эту версию фреймворка, а обратная совместимость (выбор рантайма при запуске) позволяет использовать основу до .NET Core (пока исключая).
В теории, базовые классы должны представлять из себя фундаментальную основу, позволяющие использовать их в любой ситуации. На практике же наверняка найдутся условия, для которых придётся их допилить. В этом случае весь код абстрактных классов можно переписать, а интерфейсы расширить собственной реализацией. Поэтому в этой сборке и находится самый минимум возможного кода.
На момент написания статьи единственным хостом, наследующим базовые интерфейсы, является хост для WinService приложений.
Этот набор базовых классов, который предоставляет основу для написания приложений на основе WinForms и WPF. В составе идут интерфейсы для работы с абстрактным меню, тулбаром и окнами.
С точки зрения расширения, хост как Add-In для Visual Studio расширяет интерфейсы SAL.Windows и дополняет специфичным для VS функционалом. Если зависимый плагин не находит ядра, взаимодействующего со студией, то он может продолжать работать с ограниченным функционалом.
Все написанные хосты, поддерживающие интерфейсы SAL.Core, автоматизируют следующий функционал:
На этих интерфейсах реализованы следующие хосты:
Логирование событий реализовано через стандартный System.Diagnostics.Trace. В хостах MDI, Dialog и WinService, listener прописанный в app.config'е пытается отдать полученные события обратно в само приложение через Singleton, которые затем отображаются в окнах логов (Output или EventList) в зависимости от события. Для devenv.exe тоже присутсвует возможность прописать trace listener в app.config'е, но в данном случае мы получим загрузку сборки хоста до подгрузки его в качестве Add-In'а. Поэтому trace listener добавляется программно в коде (Отображает в Output VS или диалоговым окном).
Написанная инфраструктура позволяет развиваться в направлении HTTP приложений, но для этого необходимо реализовать часть модулей, обеспечивающих как минимум аутентификацию, авторизацию и кеширования. Для приложения TTManager, которое описано ниже, был реализован свой собственный хост для WEB сервисов, который реализовал в себе весь необходимый функционал, но, увы, он сделан под конкретную задачу, а не как универсальное приложение.
Такой подход логирования и разбивания на отдельные модули, позволяет с лёгкостью выявить узкие моменты при запуске в новом окружении. Для примера, при разворачивании массива модулей на Windows 10, обнаружил, что загрузка, занимает времени намного больше, чем на другой ОС. Даже на моей старенькой машине с WinXP, загрузка 35 модулей выполняется максимум за 5 сек. Но на Win10 процесс загрузки одного единственного модуля куда больше времени.
Благодаря распределённой архитектуре, локализовать проблемный модуль удалось мгновенно. Данная проблема решена сменой рантайма с 2.0 на 4.0 в .config файле при запуске по Windows 10:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<!--...-->
<startup>
<supportedRuntime version="v4.0" />
</startup>
<!--...-->
</configuration>
Первая версия инфраструктуры появилась в 2009 году. Как для тестирования, так и для выполнения тривиальных задач по работе, накопилось большое количество разнообразных и независимых модулей, автоматизирующих разные монотонные задачи (Все картинки кликабельны).
В основе этого приложения лежит приложение, идущее совместно с Visual Studio - WCF test client. Как видно из интерфейса первоисточника, целью этой программы было не массовое использование, а тестирование внутреннего функционала WCF. К моменту перехода на WCF у меня уже было написано много приложений на обычных WebService'ах. Изучив принципы работы самой программы через ILSpy, я решил добавить поддержку не только WCF, но и WS клиентов. В итоге, разобрав основную программу, я написал плагин со следующим расширенным функционалом:
Опять же, первоисточником программы стали программисты из M$. В основе программы лежит программа RDCMan, но, в отличие от основной программы, я решил встроить окно подключённого сервера в диалоговый интерфейс. А удалёное хранилище настроек, помогло держать список серверов у всех заинтересованных коллег в актуальном состоянии.
В первоисточнике этого приложения лежит новая идея по автоматизации, которую я не смог найти в других приложениях. Цели написания написания такого приложения было 2:
Несколько раз получалось, что исполняемые файлы реализовывали некий функционал, но этот функционал либо устаревал, либо никем не использовался. Чтобы не искать по исходным кодам приложений на разных языках использование тех или иных объектов и написано это приложение. Для примера, у меня есть сборка в общем репозитории и я решил удалить из этой сборки один метод. Как узнать, используется ли этот метод в текущих зависимых сборка других проектов написанными коллегами? Можно попросить проверить всех исходный код, можно посмотреть поискать в Source Control, а можно просто поискать одноимённый метод внутри скомпилированных сборок. Оно состоит из 2х компонентов:
Клиент имеет 2 варианта поиска:
Через пользовательский интерфейс вручную, либо поиском через рефлексию, наложенную на сборку PEReader.
UI представляет из себя дерево директорий и дерево метаданных со всеми таблицами. В следующей версии я хочу добавить развёрнутый вид привычной иерархии метаданных, но не более, ибо для чтения тонких и толстых методов есть инструменты гораздо удобнее.
Компонент поиска по PE файлам представляет из себя рефлексию свойств и классов сборки PEReader, с возможностью выбора нужного элемента PE файла и указания требуемого для поиска ключа и значения. После выбора папки поиск будет осуществлён по всем найденным файлам с правильными PE заголовками.
Чтобы не описывать каждым отдельным пунктом весь список готовых модулей, я опишу оставшиеся модули одним списком:
Остальные тут.
Изображения всех модулей тут.
Для наглядного демонстрирования удобств построения всего комплекса на модульнйо архитектуре, я приведу пару готовых решений построенных на разных принципах:
Приложение для системы задач, которое в основе использовало систему динамического расширения с возможностью использования разных источников задач. В итоге получился унифицированный интерфейс, который способен создавать, экспортировать/импортировать, просматривать задачи из разных источников. На текущий момент поддерживает в качестве источника MSSQL, WebService и частично REST API тасков Мегаплана (не реклама). WebService написан по аналогичному принципу с использованием базовых классов SAL.Web. Так что сам WebService также могут использовать в качестве источника MSSQL, Мегаплан или опять WebService.
Kernel плагин приложения, ленивой загрузкой ищет все плагины источников задач (DAL). Если найдено несколько плагинов доступа к данным, то клиенту предлагается выбрать тот плагин, который он хочет использовать (Только в SAL.Windows, в хостах без пользовательского интерфейса - вылетит с ошибкой). Зависимые плагины получают доступ к выбранному DAL плагину через Kernel.
В данном примере Kernel плагин абстрагирован интерфейсами от остальных зависимых плагинов. В таком случае, можно написать ещё один Kernel модуль (или переписать текущий. Или переписать вообще любой плагин) для возможности работать с несколькими источниками задач одновременно. Для решения проблемы со статусами задач, внутри некоторых DAL плагинов зашита матрица статусов (Или берутся из источника задач, если есть). В таком случае не возникает проблем с переносом данных из одного источника в другой.
Приложение позволяет, используя готовые плагины, парсить сайты через Trident или WebRequest. Для парсинга доступно несколько уровней абстракции. Самый низкий уровень позволяет написать дополнительный плагин, который будет заниматься открытием и парсингом ответа, используя DOM или ответ от сервера. Уровень выше предлагает написать .NET код в рантайме, который через плагин “.NET Compiler” будет скомпилирован и применён к результату страницы, отображаемой в Trident'е в рантайме. Самый высокий уровень предполагает указание, через UI, элементов на странице сайта отображаемой в Trident’e. И после применения xpath (самописный вариант) шаблона, передать на обработку в универсальный плагин или выполнить .NET код из плагина ".NET Compiler".
Модулю, зависимому от Kernel плагина, предлагается выбрать один из готовых интерфейсов вывода и базовый пользовательский интерфейс скачивания данных. Либо Trident, либо WebRequest с возможностью логирования. Kernel предлагает не только интерфейс, но и таймер опрашивания каждого отдельного модуля.
Интерфейс вывода предлагает стандартный GridView с контейнером вывода данных, с возможностью сохранения последней открытой позиции в таблице. По умолчанию контейнер поддерживает отображение изображения или текстовых данных.
В данном случае я не стал абстрагироваться от Kernel плагина интерфейсами и все зависимые плагины ожидают найти в массиве пдогруженных плагинов конкретный Kernel плагин.
Приложение писалось в 3 итерации (Только под SAL.Windows):
Приложение, написанное на WPF и использует свой собственный хост для взаимодействия с модульной архитектурой. В данном случае, SAL модули преставляют из себя плагины, для расширения функционала приложения.
Хост EnvDTE проверен только на английских студиях. Могут возникнуть проблемы на локализованных версиях (Один раз испытал на VS11 с русской локализацией).
Хост EnvDTE закрывает студию, если подгружен плагин Winlogon (SENS) и пользователь решил выгрузить хост через Add-in Manager. (Встретил на Windows 10).
Т.к. Хост написан как Add-In, а не как полноценное расширение, то совместимости с другими продуктами на основе EnvDTE - отсутствует.
Вы решили использовать функции кеширования, в довесок к встроенным классам System.Web.Caching.Cache и System.Runtime.Caching.MemoryCache, доступны удалённые кеши. Для примера, AppFabric. Написав базовый интерфейс для кеширования, можно разработать массив модулей для каждого вида кеша и выбирать нужный модуль по необходимости (На момент публикации уже написаны, но не выложены).
Модули на момент написания могут подгружаться с файловой системы, с файловой системы в память и обновляться по сети, используя в качестве TOC XML файл. Дальнейшее развитие позволяет использовать в качестве хранилища не только nuget, но и запускать модули на удалённом хосте.
Персонализация пользователя возможна как Roles, так и Claims. Но при использовании OpenId, OAuth, OpenId Connect, провайдеров существуюет огромное множество, при этом от каждого провайдера требуется получить System.Security.Principal.IIdentity (При использовании Roles based auth) или System.Security.Claims.ClaimsIdentity (При использовании Claims аутентификации). Соответственно, один раз написав клиента для LinedIn'а, можно его использовать в любом приложении без перекомпиляции.
При использовании очередей сообщений можно написать модуль, который будет выполнять функции ServiceBus, а модули реализации конкретной очереди уже будут отвечать за получение и отправку сообщений.
Можно написать UI интерфейс динамического связывания модулей, по аналогии с SSIS или BizTalk сервисами.
Если кому будет интересен такой подход к разработке, то я могу выложить исходный код части написанных модулей в открытый доступ. Увы, с текущей организацией моего рабочего кода, выложить всё одним скопом — проблематично.