Как стать автором
Обновить

Комментарии 19

Спасибо за статью. Пришел примерно к такому же решению, но пока без прекомпилированных вьюх. И информацию о плагине я бы не стал брать из сборки, так как вы будете вынуждены ее загрузить. Если брать манифест в файлике, то вы еще до загрузки сборки можете проверить, например, совместимость версий.
Еще классно можно подцепить модули в виде Nuget пакетов ;)
Спасибо.
Если брать манифест в файлике, то вы еще до загрузки сборки можете проверить, например, совместимость версий

Идея хорошая. Попробую обязательно как будет время.
Еще классно можно подцепить модули в виде Nuget пакетов ;)

Согласен, идея очень хорошая. Но я сейчас занимаюсь преобразованием компонентов в шаблоны. Базовую систему в шаблон и плагин в шаблон для ускорения разработки.
MEF — мощный инструмент, но в контексте модульной системы для сайтов, на мой взгляд, слишком тяжеловесна.
Для Asp.Net приложения там возникают разные нюансы. Например в плане загрузки сборок — shadow coping сборок перед загрузкой приложения и пробинг их из приватной папки веб приложения. Плюс MEF не решает вопросов с ресурсами(js, css, cshtml).
А можно подробнее, чем не устроили Areas?
С помощью того же RazorGenerator они отлично выносятся в отдельные dll'ки и получается, как мне кажется, нечто очень похожее на ваш конечный результат. Или я что-то упускаю?
Области не устроили только тем, что разработка плагина с их помощью не может быть выделена в написание/тестирование отдельного MVC проекта, который в конечном итоге компилируется в один единственный файл. Но я могу ошибаться, прошу поправьте.
Вот мне тоже так показалось, поэтому я и привел ссылку в комментарии :)
Да, Areas можно достаточно просто выносить в отдельные dll'ки.
Спасибо за статью!

Кстати, вышеописанный подход очень (!!!) похож на существующую реализацию системы плагинов в nopCommerce (http://www.nopcommerce.com) и Umbraco (http://umbraco.com/). Я бы даже сказал, что Вы переписали эту систему плагинов оттуда, просто переименовав названия классов и немного упростив код.

Кстати, статью лучше назвать «Плагинная система на ASP.NET», а не «Плагинная система на MVC», так как все вышеописанное (кроме примера с Layout.cshtml) можно реализовать и в стандартных ASP.NET web forms
Честно говоря никогда не смотрел внутрь Umbraco и nopCommerce, поэтому не могу ничего сказать по поводу схожести. Как я уже написал, конечная система родилась в результате сбора информации в сети и создания конечного проекта.
Конечно, я потратил больше времени при таком подходе, но считаю, что сделал это не зря, потому что разобрался с подходом и его функционированием.
Я не утверждаю, что вы «заимствовали» код из этих систем. Но если вы взгляните на их реализацию, то поймете о чем я говорю.
Классно! В свое время, еще на ASP.NET 1.1 сделал загрузку сборок через Reflection с дополнительным функционалом к основному ядру сайта.
Модули конфигурировались в одну строчку в определенном хмл файле

type id='AbsType' manager='extman.EventFestDopInfoManager;EventDopInfo.dll'

Тем самым, базовый функционал можно было безгранично расширять, без перекомпиляции ядра.
В модуле переопределялись методы базовых классов из ядра. Добавлялись новые классы ну и т.д. Страницы (вьюхи в нотации MVC) правда, мы туда не складывали, но ничто не мешает это делать.
Так как технология Web Forms категорически не устраивала, только с приходом MVC появилась возможность перевести весь проект на новые фрэймвоки. Вообще конечно жалко, что не было MVC в то время не было. Web Forms вселяло беспросветную тоску. Пришлось выкручиваться и придумывать все с нуля.

Будем переводить сразу на 4.5.
Думаю этот пост мне поможет в этом.
Если абстрагироваться от MVC и вообще от ASP. NET. Подобная идея отлично подходит для построения любого модульного приложения.

Запускать модули как отдельный сайт это интересная идея. Для тестов подойдет, нужно попробовать.
Что-то вы слишком усложняете. Проще надо быть, проще…

Узнать суть
Например:
//в целях упрощения примера пусть абсолютно все контроллеры будут плагинами. При этом в «плагине» может быть много контроллеров. Структура плагина — веб проект с стандартной asp.net mvc структурой каталогов — bin\ — бинарь, Views — вьюхи.

1) рисуем и тестируем свои контроллеры как обычно, но роуты проставляем атрибутами (для mvc4 используем AttributeRouting, для mvc5 — встроенный). Я буду рассматривать mvc5 — различий не особо много. Например HomeController в плагине HomePlugin выглядит так
namespace HomePlugin.Controllers
{
    public class HomeController : Controller
    {
        [Route(""), HttpGet]
        public ActionResult Index()
        {


            return View();
        }
    }
}


2) пока забудем о Lazy загрузке контроллеров (для этого надо писать ControllerFactory, что в комменте не очень удобно, да и ничего в этом сложного). Будем регить плагины в веб конфиге хост-приложения.
Для этого пусть у нас есть еще и хост приложение. Опять же стандартный пустой MVC5 проект. Добавим авто регистрацию роутов по атрибутам:
[assembly: WebActivatorEx.PostApplicationStartMethod(typeof(RouteConfig), "RegisterRoutes")]

namespace HomePlugin.App_Start
{
    public class RouteConfig
    {
        public static void RegisterRoutes()
        {
            RouteCollection routes = RouteTable.Routes;
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapMvcAttributeRoutes();
        }
    }
}

(не забудем установить nuget пакет WebActivatorEx)

3) Наши плагины пусть будут копироваться в bin каталог хост-приложения (вместе с вьюхами). Т.е. структура хост приложения у нас будет такая: bin\HomePlugin\bin — бинари HomePlugin плагина, bin\HomePlugin\Views — вьюхи хоум плагина.

Регим загрузку нашего плагина хост приложением вписывая его в веб конфиг:

<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.5.1">
      <assemblies>
        <add assembly="HomePlugin"/>
      </assemblies>
    </compilation>
    <httpRuntime targetFramework="4.5.1" />
  </system.web>

  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <probing privatePath="bin\HomePlugin\bin;" />
    </assemblyBinding>
  </runtime>
</configuration>



4) Запускаем и получаем болт в виде «не могу найти вьюху для HomeConteroller-а Index экшена.
Окей, это потому что они лежат не там где ожидает их RazorViewEngine — напшем свой ViewEngine и попутно его зарегим (это в хост приложении естественно):
[assembly: WebActivatorEx.PostApplicationStartMethod(typeof(RegisterViewEngine), "Start")]


namespace WebAndPlug.App_Start
{
    public static class RegisterViewEngine
    {
        public static void Start()
        {
            ViewEngines.Engines.Clear();
            ViewEngines.Engines.Add(new PluginViewEngine());
        }
    }


    public class PluginViewEngine : RazorViewEngine
    {
        #region Overrides of VirtualPathProviderViewEngine

        public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
        {
            var ctrlAssembly = controllerContext.Controller.GetType().Assembly.GetName().Name;
            string controller = controllerContext.RouteData.GetRequiredString("controller");
            string action = controllerContext.RouteData.GetRequiredString("action");
            viewName = "~/bin/" + ctrlAssembly + "/Views/" + controller + "/" + viewName + ".cshtml";
            return base.FindView(controllerContext, viewName, masterName, useCache);
        }


        public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
        {
            return base.FindPartialView(controllerContext, partialViewName, useCache);
        }


        #endregion
    }
}


и вуаля — у нас плагины и все дела… (для партиал вьюх аналогично, скрипты и прочее — через спец контроллер отдавайте из нужных каталогов).

Мы с командой сейчас работаем с Orchard CMF.
Очень гибкая система, также построенная на Area (но специфичные). Но он тоже DLLки закидывает не в bin, а в App_Data. Но, если поковыряться, то уверен, можно будет переделать под свои нужды. Для загрузки модулей/плагинов используются реализации IExtensionLoader.
Чтобы избежать копирования сборок перед загрузкой можно использовать AppDomain.CurrentDomain.SetShadowCopyPath и передать ему путь к каталогу со сборками. Этот метод помечен как Obsolete, но работает как надо.
Проблемы нет, так как каждый модуль может разрабатываться как отдельный сайт.
Также нет проблем с передачей моделей.
Единственное неудобство, которое я заметил для себя — трудности с использованием связок (bundles). Но я работаю над этим. Возможно удастся что-то сделать.
Хотелось бы услышать за что минусуют статью
Трудно живется .NET программистам без OSGi :)
Недостатки текущей реализации которых не было бы в OSGi:
— общие ресусры
— общее пространство имен с базовым сайтом.
— недостаточный контроль зависимостей между плагинами (можно ли сделать один плагин зависящим от другого?)
— не понятно как реализовать взаимодействие плагинов между собой.
— необходимость перезапуска приложения для установки / удаления плагина.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории