Доброго времени суток, уважаемые читатели. В данной статье я расскажу вам о развитии плагинной системы для проектов, написанных на ASP.NET MVC. В предыдущей статье я описал основы создания системы, позволяющей разделить её части на отдельные плагины.
На написание продолжения меня подтолкнули многочисленные вопросы, которые я получаю от читателей. Продолжение под катом.
Вступление
Мне пришло большое количество вопросов по описанной системе от пользователей хабра. Очень приятно, что многим оказалась интересна и полезна моя статья. Чаще всего меня спрашивают о дальнейшем развитии системы. В связи с некоторыми изменениями в жизни, мне пришлось немного отойти от данной темы. Но учитывая интерес к теме, сегодня я постараюсь поделиться с вами своим опытом внедрения и проблемами, с которыми я столкнулся при работе с системой.
Как и предыдущая статья, данная написана для новичков, которым может быть сложно понять все тонкости. Буду описывать всё максимально простым языком (в некоторых местах опытные программисты могут начать закидывать меня помидорами за упрощения), поэтому прошу прощения у особо чувствительных к изложению читателей прощения.
Проблемы и ограничения в первой версии
Описанная мной ранее система имеет определенные плюсы при работе над большими проектами. Но, как и во всём, она также имеет свои недостатки. В ходе внедрения системы в рабочие проекты я столкнулся с несколькими неприятными моментами:
- Файлы скомпилированных плагинов блокируются после загрузки, что не позволяет обновлять из «налету».
- Необходимо постоянно отслеживать версии плагинов в папке, чтобы не были подгружены старые версии.
- Необходимость подключать сторонние библиотеки к корневому проекту для обеспечения работы плагинов.
Это не полный список проблем, с которыми я столкнулся при работе. Но эти проблемы являются базовыми и из-за них разработка может превратиться в очень неприятное действо (сколько боли я испытал из-за них и кровавых слёз пролил не счесть).
Список проблем определён, будем их решать.
Блокировка плагинов
В первой версии системы есть очень большая проблема — блокировка файлов плагинов. И эта проблема встаёт очень остро, если плагин подключается к «боевому» проекту, остановка которого крайне нежелательно. Для обновления плагинов требовалось так или иначе останавливать сайт и загружать обновленные версии компонентов системы.
Данная проблема вытекает из одной строчки кода:
var currentAssambly = AppDomain.CurrentDomain.Load(assembly);
Поясню что тут происходит, если нет опыта работы с доменами приложений. Для начала посмотрим в MSDN на определение класса домена приложения.
Представляет домен приложения, являющийся изолированной средой, в которой выполняются приложения.
Для более простого понимания представьте, что домен — отдельный процесс в системе, в котором выполняется приложение. Таким образом из строчки кода выше мы можем понять, что происходит «подгрузка» файла плагина в процесс основного сайта. Из этого следует, что файл будет заблокирован до тех пор, пока существует домен приложения (или запущен сайт).
Для решения проблемы блокировки существует несколько общепринятых решения:
- Загрузить файл плагина в собственный домен.
- Вызвать перегруженный метод .Load(byte[]) для домена. Этот метод позволяет выполнить подключение сборки по копии его содержимого из байтового массива.
- Включить shadow copy для домена приложения и загрузить плагины статическим методом Assembly.Load()
Первый способ плох тем, что порождает отдельные процессы для плагинов и изолирует (на самом деле, при правильной настройке домена это решается, но требует написания большого количества дополнительного кода) загружаемый плагин от основного сайта и его библиотек. Так что его использовать не будем.
Второй способ нам не подойдёт, так как в данном случае, также как и в первом, плагин изолируется от библиотек. Да и сам MSDN говорит о том, что метод .Load(byte[]) является вспомогательным и используется только если нет никакой возможности вызвать статический метод Assembly.Load(). В нашем случае ничто не мешает воспользоваться статическим методом.
А вот третий способ мы возьмём на вооружение. Он требует минимальное количество кода и прост для встраивания в уже написанный менеджер плагинов.
string cachePath = @"C:Temp";
if (!System.IO.Directory.Exists(cachePath)) System.IO.Directory.CreateDirectory(cachePath, new System.Security.AccessControl.DirectorySecurity());
AppDomain.CurrentDomain.SetCachePath(cachePath);
//Set shadowcopy to prevent locking plugins
AppDomain.CurrentDomain.SetShadowCopyPath(AppDomain.CurrentDomain.BaseDirectory);
AppDomain.CurrentDomain.SetShadowCopyFiles();
Немного поясню. При использовании Shadow Copy домен приложения перед загрузкой плагина (или иной подключенной библиотеки) делает её копию в указанную папку и работает с этой копией, давая возможность манипулировать исходным файлом.
В приведенном коде мы указали путь к папке, где будут храниться копии загружаемых плагинов. Не забывайте, что папка должна быть доступна для записи. Иначе при старте мы получим исключение.
После загрузки в домен приложения плагина, его уже невозможно убрать или заменить. Таким образом, хоть файлы плагинов и доступны для перезаписи «налету», необходимость в перезапуске не отпадает.
Отслеживание версий плагинов
Эта проблема, возможно, возникает только у ограниченного круга разработчиков (не исключаю, что это круг ограничен мной одним), но раньше я мог по ошибке выгрузить обновленную версию плагина не в ту папку или просто поменяв имя файла выложить сразу две версии.
Кривизна рук и никакого мошенничества. С другой стороны, иметь возможность хранить сразу несколько версий плагина на сервере может оказаться полезной. Например, если в новой версии происходит критический сбой, а программист заболел. Ситуация, конечно, выдуманная и фантастическая, но мало ли. Поэтому мы добавим отслеживание версий. Делается это достаточно просто. Проходя по всем файлов плагинов рефлексией получаем версию и добавляем в список подключаемых плагинов. Если в списке уже есть такой плагин, выполняем сравнение версий. Если найденный файл имеет старшую версию, заменяем.
...
IList<System.Reflection.AssemblyName> assemblies = new List<AssemblyName>();
foreach (var dll in libs)
{
...
//Проверяем наличие плагина в списке
if (!assemblies.Any(o => o.Name == dll.Name))
assemblies.Add(dll);
//Replace assembly with higher version
//Если плагин есть, сравниваем версии и добавляем более новую
else if (assemblies.Any(o => o.Name == dll.Name && o.Version < dll.Version))
{
assemblies.Remove(assemblies.FirstOrDefault(o => o.Name == dll.Name));
assemblies.Add(dll);
}
...
Подключение сторонних библиотек
Эта проблема у меня вызвала больше всего боли и страданий. Сколько раз я натыкался на ситуацию, когда я не вижу новый плагин в системе. И только дебаг показывал ошибку загрузки подключенной библиотеки. Да и засорять базовый проект не комильфо. Поэтому было принято решение создать своего рода репозиторий библиотек, в который бы складывались подключенные библиотеки. Но есть один нюанс, на который тут стоит обратить пристальное внимание. Если вы обновляете версию библиотеки в одном из плагинов, а в других осталась старая версия — получим ошибку. Но это вопрос внимательности. После пары обновлений в голове откладывается эта особенность и дальше проблемы пропадают.
В данном случае для решения задачи нам необходимо всего лишь добавить одну в строчку конфига (основного проекта) следующее:
<probing privatePath="bin;plugins;plugins/SharedLibs" />
Я создал директорию SharedLibs. Но название, как вы понимаете, может быть любым. Таким образом мы говорим основному проекту, что искать библиотеки он должен в указанных в параметре местах. Всю остальную магию по разрешению зависимостей за нас сделает .NET
Послесловие
Итак, мы сделали вторую итерацию по разработке плагинной системы для сайтов на ASP.NET MVC. Основные проблемы, вызывавшие трудности в использовании системы, решены. На данном этапе развития идеи система уже используется в продакшн проектах и показывает свою жизнеспособность.
Статья получилась небольшой потому что является своего рода дополнением к предыдущей.
Сейчас работаю над автоматической заменой обновленных плагинов «налету». Но текущая реализация очень нестабильна и выкладывать её будет неприлично.
Также надеюсь, что хотя бы немного удовлетворил интерес всех, кто писал мне. С удовольствием отвечу на ваши вопросы.
P.S. Друзья, ваша конструктивная критика очень важна. Высказывайте своё мнение в комментариях.
P.P.S. Времени крайне мало и не хватает на реализацию всех идей и задумок. Если найдутся энтузиасты и заинтересованные люди, присоединяйтесь к проекту на github. Будем вместе развивать систему.
Автор: ParaPilot