Model View Dispatcher (cqrs over mvc)

imageДоброго всем времени суток, в этой статье хочу осветить ещё один компонент из библиотеки Incoding Framework.Model View Dispatcher (MVD) — позволяет избавится от избыточного кода (а именно asp.net mvc controller) и упростить навигацию по проекту, уменьшив количество абстракций между клиентским и серверным кодом.На хабре имеются несколько статей о IML и CQRS, которые входят в состав framework

Знакомство с IML Incoding CQRSпримечание: статья была опубликована, но в то время были проблемы с аккаунтом на хабре и она прошла не замеченной IML vs AngularJsпримечание: статья подверглась критике (объективной и субъективной) Ответ на IMl vs AngularJsпримечание: нельзя назвать успехом, но уже немного лучше MVD фигурировал в некоторых статьях, но как часть примера демонстрирующая возможности IML, поэтому я решил рассказать о нем отдельно, но обо всем по порядку…Зачем? Рассмотрим сценарий применения asp.net mvc + cqrsController public ActionResult Details (GetUserDetailsQuery query) { var model = dispatcher.Query (query); return Json (model); } примечание: action отвечает за binding и передачу query в Dispatcher для возврата полученных данных в виде Json View $.get ('@Url.Action («Details», «Controller»)', callback) примечание: не хватает листинга Query для полной картины, но становится очевидно избыточность ActionДиета Чтобы посадить controller на «диету» можно воспользоватся паттерном Mediator (как один из вариантов), который опирается на Interface и Generic, но это только позволяет упростить и объединить код, но не решить проблему полностью, потому что все равно приходится писать однотипные Controller/Action.Model View Dispatcher (MVD) — позволяет выполнять Command/Query в «обход» asp.net mvc. Для демонстрации перепишем предыдущую задачу, но с MVD

$.get ('@Url.Dispatcher ().Query (new GetUserDetailsQuery ()).AsJson ()', callback) Подсчитаем бонусы Не нужен Action Проще навигация, потому что теперь действия на странице отражают картину (go to delcaration без посредников) на серверного кода Строгая типизация при построении URL (visual studio intelisence, refactor utilities) Нет MVC??? Чтобы получить ответ, посмотрим как подключить MVD к проекту: Создать DispatcherController (начиная с версии 1.1 устанавливается через nuget), который унаследовать от DispatcherControllerBase

public class DispatcherController: DispatcherControllerBase { public DispatcherController () : base (typeof (T).Assembly) { } } примечание: конструктор принимает Assembly в котором объявлены Command/QueryDispatcherControllerBase содержит следующие методы (Actions):

Query (string incType, string incGeneric, bool? incValidate) Render (string incView, string incType, string incGeneric) Push (string incType, string incGeneric) Composite (string incTypes) QueryToFile (string incType, string incGeneric, string incContentType, string incFileDownloadName) пример url, после вызова которого будет выполнен Push command Url.Action («Push», «Dispatcher», new { incType = typeof (Command).Name }) Альтернативный способ через DSL (domain specific language) Url.Dispatcher ().Push (new Command ()) примечание: дело не только в лаконичности синтаксиса (хотя это важно), но также в абстракции от деталей (имена параметров и т.д.)Обратить внимание

MVD использует IDispatcher, который зарегистрирован в IoCFactory (пример со StructureMap в качестве провайдера) В качестве ActionResult возвращается IncodingResult, который формирует JSON{ success: true/false, data: something/null, redirectTo: url/null } Вывод: текущая реализация MVD опирается на asp.net mvc, по факту просто делает обобщенный (не generic) Controller, но можно использовать httphandler или другой http обработчик в качестве платформы.

Что умеет ? MVD покрывает большинство сценариев, которые встречаются при веб-разработке на платформе asp.net mvc: примечание: исходный код примеров на GitHubPush Url.Dispatcher ().Push (new AddUserCommand { Id = »59140B31–8BB2–49BA-AE52–368680D5418A», Name = «Vlad» }) примечание: вопросы валидирования далее Push generic Url.Dispatcher ().Push (new AddEntityCommand()) Сomposite Url.Dispatcher () .Push (new AddUserCommand { Id = »1», Name = «Name» }) .Push (new ApproveUserCommand { UserId = »2» }) Query as json Url.Dispatcher () .Query (new GetCurrentDtQuery ()) .AsJson () Query generic Url.Dispatcher () .Query (new GetTypeNameQuery()) .AsJson () Query as view Url.Dispatcher () .Query (new GetUserQuery ()) .AsView (»~/Views/Home/User.cshtml») примечание: путь к View строится от корневой (в asp.net mvc относительно Controller) директории сайта, что позволяет строить любую структуру папок. Query as file Download примечание: требуется реализация QueryBase для Query Model as view Url.Dispatcher () .Model (new GetUserQuery.Response { Id = »2», Name = «Incoding Framework» }) .AsView (»~/Views/Home/User.cshtml») View Url.Dispatcher ().AsView (»~/Views/Home/Template.cshtml») Сценарии MVD состоит из DispatcherController (серверная часть), который используя инфраструктуру CQRS выполняет Command/Query и Url.Dispatcher (адрес строится на сервере, но далее используется на клиенте) для построения url, но не является самостоятельным компонентом, поэтому примеры будут в контексте с IML.Post @(Html.When (JqueryBind.Change) .AjaxPost (Url.Dispatcher ().Push(new {Value = Selector.Jquery.Self () })) .OnSuccess (dsl => dsl.Utilities.Window.Alert («Success»)) .AsHtmlAttributes () .ToCheckBox (true)) примечание: в качестве параметра используется анонимный объект, но проверка на соответствие (если Value отсутствует в AddAcoGroupCommand будет exception) полей будет. Post form @model AddAcoGroupCommand

@Html.HiddenFor (r=>r.Id)
Render View @(Html.When (JqueryBind.InitIncoding) .AjaxGet (Url.Dispatcher ().AsView (»~/Views/Patient/BenefitListControl.cshtml»)) .OnSuccess (dsl => dsl.Self ().Core ().Insert.Html ()) .AsHtmlAttributes () .ToDiv ()) примечание: удобно для поиска template var urlTmpl = Url.Dispatcher ().AsView (»~/Views/Medication/MedicationTmpl.cshtml»); dsl.Self ().Core ().Insert.WithTemplateByUrl (urlTemplate).Append (); Render model @(Html.When (JqueryBind.InitIncoding) .AjaxGet (Url.Dispatcher () .Model (new BenefitModel () { GroupName = Selector.Incoding.QueryString(r=>r.GroupName), IsPrimary = true }) .AsView (»~/Views/Patient/BenefitListControl.cshtml»)) .OnSuccess (dsl => dsl.Self ().Core ().Insert.Html ()) .AsHtmlAttributes () .ToDiv ()) примечание: возможность использовать Selector (вычисляется на клиенте) при формировании url позволяет не прибегать к построению routes на клиенте. примечание: больше примеров в Inc-todoAction Attributes Это мощный механизм для реализации АОП (Аспектно-ориенти́рованное программирование‎), частым сценарием где он применяется, может быть проверка авторизации, поэтому рассмотрим возможные пути решения: Отметить атрибутом DispatcherControllerпримечание: способ очень удобен, если Вы пишите CRM систему и все действия проходя с авторизацией Dispatcher event — поскольку все Command/Query выполняются через единую точку, это позволяет реализовать аналогичное поведение как при Action attributes.примечание: акцент с Action сдвигается на Command/Query Валидация В рамках Incoding Framework есть готовая инфраструктура для валидации (в отличии от js framework тут Server/Client покрытие) и MVD интегрирован с ней. @(using (Html.When (JqueryBind.InitIncoding) .Direct () .OnSuccess (dsl => dsl.Self ().Core ().Form.Validation.Parse ()) .When (JqueryBind.Submit) .PreventDefault () .Submit () .OnError (dsl => dsl.Self ().Core ().Form.Validation.Refresh ()) .AsHtmlAttributes () .ToBeginForm (Url.Dispatcher ().Push (new AddUserCommand ())))) { @Html.TextBoxFor (r=>r.Name) @Html.ValidationMessageFor (r=>r.Name) } примечание: поскольку код будет однотипный для большинства форм, то можно написать html helperКод знаком большинству разработчиков на asp.net mvc по стандартному Html.BeginForm (можно использовать его передав iml в html attributes), но с несколькими отличиями: При первом появление элемента на странице делаем разбор текущей формы на наличие валидации По событию Submit останавливаем поведение по умолчанию (чтобы форма не отправилась) и используя ajax делаем post на указанный адрес (Url.Dispatcher ().Push (new AddUserCommand ()) В случае возраста ошибки (пользовательской, а не фатальной) в которой обновляем форму на основе Model State (ниже подробней) Для понимания, как работает метод Validation.Refresh посмотрим реализацию Push в dispatcher contoller

if (! ModelState.IsValid) return IncodingResult.Error (ModelState)

try { dispatcher.Push (composite); return IncodingResult.Success (); } catch (IncWebException exception) { foreach (var pairError in exception.Errors) { foreach (var errorMessage in pairError.Value) ModelState.AddModelError (pairError.Key, errorMessage); } return IncodingResult.Error (ModelState) } примечание: реальный код несколько сложнее из-за дополнительной логики, но пользователь Incoding Framework абстрагирован от этих деталей и работает с TryPush или MVD pushВ catch мы перехватываем только IncWebException (на основе которого заполняем ModelState), а остальные exception считаем провальными и оставляем их обработку на совесть global.asax.А как было раньше? Ответ поможет проанализировать, чем же решение на базе Incoding Framework лучше того, что имеется в стандартном asp.net mvc public ActionResult Add (AddUserCommand command) { if (ModelState.IsValid) return View (command);

return Execute (command); } Если ModelState содержит ошибки, возвращается View, которое строится на основе command (хорошо, если не используется Container с дополнительными списками, которые тоже придется заново строить), чтобы сохранить состояние. Такое поведение введет к следующим проблема: Заново строится форма, что занимает время и увеличивает трафик Возвращаемый результат является html (пусть и упакованный в json), что не позволяет повторно использовать action, как API для сторонних (мобильных) приложений Итог MVD является очень хорошим союзником для борьбы с однотипными Action, которые приводят к «разбуханию» кода, что особенно критично на поздних этапах проекта. MVD можно использовать без IML, но тогда теряется возможность использовать Selector в routes, что негативно скажется на типизации, из-за того, что придется в «ручную» (pure js) собирать параметры.Конечно, могут быть сценарии с которыми MVD не способен (временно или просто не возможно реализовать) справится, но ничто не мешает написать Controller и Action для конкретных (может это всего 5% — 10%) случаев.Получается все ради того, чтобы убрать дубляж ? Если MVD поможет сократить код на 10–15% и ускорить разработку, то это уже очень хороший результат, но мы пошли дальше и реализовали возможность строить схему end point (идея взята у wcf endpoint)…Исходный код, как документация ? Диалог между разработчиком api и мобильного приложения: Api — я добавил новый запрос, по адресу /GetUsers? Active=true Api —, а также новые поля для создания user (Comment, City, State) Api — о, чуть не забыл City это справочник и его можно получить по запросу /GetCities Api — и ещё один момент, заказчик просил, чтобы State был числом, так что учти это Мобильное приложение — ок, опиши в документе и назови %sitename%-api-%current-version%примечание: основной проблемой является %current-version%, потому что приходится поддерживать актуальность документации, после каждого изменения в коде Тот же диалог, но с mvd end-points Api — обновил код Мобильное приложение — уже можно смотреть appDomain/Dispatcher/Endpoints? Api — конечно, там кстати песочница где можно проверить command/queryпримечание: песочница, так же удобна и для разработчика API, так как часто разработка идет без UI (user interface) и чтобы быстро проверить на работоспособность (unit test покрывает код, но интеграционные тоже нужны) можно воспользоватся авто-сгенерированной формой примечание: пример такой страницы, которая находится на ранней стадии разработки, но отражает общую картину, то есть по большей части не хватает внимания к деталям, чтобы упаковать это решение в готовую библиотеку.Вывод: возможность строить документацию на основе исходного кода, повышает обратную связь между разработчиками API и мобильного приложения. Кроме документации, можно расширить функционал, реализовав статистику запросов, профилирование и многое другое, что становится возможным реализовать из-за унифицированного кода, который можно разбирать и анализировать через рефлексию.

P.S. опубликована новая версия (пока beta) Incoding Framework в nuget, где появилось много нововведений и доработок. Я приведу несколько, а остальные постараюсь расписать в отдельных статьях (полный список на нашем bugtracker)

Упрощения условий Break.If (r=>r.Is (()=>Selector.Jquery.Self ()).And.Is (()=>«id».ToId ()== 12)) // Old Break.If (()=>Selector.Jquery.Self () && id.ToId () == 12) // New Реализация EF, RavenDB, MongoDb провайдеров (одна command и разные ORM на github и inc-todo-ravendb) Упрощения синтаксиса inDsl.Core ().JQuery.Attributes.SetAttr (HtmlAttribute.Checked) // Now inDsl.Attr.Set (HtmlAttr.Checked) // Future примечание: причина статуса beta (в конце мая уже будет стабильная версия) в том, что ещё не все наши проекты переведены на эту версию и остался ряд задач, которые планировали для этого релиза

© Habrahabr.ru