Как навести порядок в
коде Web-приложения
АндрейЧебукин
Андрей Чебукин (XperiAndri)
Azure архитектор
DevOps автоматизатор
Любитель: F#, Swift, functional
programming
MCSD:Windows 8 C#
Сооснователь:The Secret Circle
Solutions
Преподаватель: КА ШАГ
Студент-партнёр Microsoft: Одесса
Повестка
 К чему приводит традиционная 3-х уровневая архитектура
 CQRS
 Медиатор
 CQRS + MediatR + AutoMapper = порядок
Традиционная 3-х уровневая архитектура
Сделаем ка ORM модель
И фигнём модель прямо из контроллера
public virtual ActionResult Get(long id)
{
Binder result = _service.Get(id);
return JsonNet(result.Flat((List<object>)null));
}
И что вышло?
 Модели БД используются в бизнес-логике
 И из чего состоит слой бизнес-логики?
Мега service
public interface IBinderService
{
Binder Get(Guid Id);
List<Binder> GetAsync();
Binder Save(Binder binder);
Binder Update(Binder task);
Binder Create(Binder task);
void Delete(Guid id);
Section GetSection(long sectionId);
}
Мышление основанное на CRUD
 Формы не удобные в использовании
 Сложно поддерживаемый, непонятный код с уязвимостями
 CRUD anemic model
 DTO нас не спасут
Что можно сделать?
Отдельные DTO на чтение, создание и изменение
Пойдём дальше!
Разделим чтение и запись вообще
CQS vs SQRS
CommandQuery
Separation
 Метод-команда
 Метод-запрос
CommandQuery
Responsibility Segregation
 Модель команды
 Модель запроса
Состав CQRS
 Запрос
– Retrieve
 Команда
– Что делать
 Событие
– Что произошло
 Сага
Команда vs Событие
Команда
 Должна быть выполнена
 Обработчик производит
действие
Событие
 Можем реагировать, а можем
не реагировать
 Обработчик реагирует на
действие
Команда vs DTO
Команда
 Сериализованный вызов
метода
DTO
 Контракт между слоя
приложения
 Для обратной совметимости
Демо
CQRS
Может ли команда отправлять
команду?
Нет
Сага
 Из команды команду вызывать нельзя
 Нужно сделать сагу, которая будет реализовывать рабочий
процесс, который посылает команды
Варианты реализации
 ORM/ADO.NET
 Optimized indexed views/stored procedures
 Full-text search/ElasticSearch
Шаблон «Медиатор»
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using System.Web.Mvc;
using App.Entities;
using App.Models.Upload;
using App.Services.Admin.LogIntegrator;
using App.Services.AuthenticationServer;
using App.Services.DatabaseManagement;
using App.Services.DataImport;
using App.Services.DataImport.BO;
using App.Services.DataImport.Dal;
using App.Services.Forecast;
using App.Services.Forecast.Bo;
using App.Services.Helpers;
using App.Services.History;
using App.Services.Logging;
using App.Services.MultiTenancy;
using App.Services.Perimeters;
using App.Services.Settings;
using App.Services.ThirdParty;
using App.Services.ThirdParty.Bo;
using App.Services.TimeLevel;
using App.Services.User;
using App.Web.Filters;
using App.Web.Helpers;
using App.Web.Hubs;
using App.Web.Models.HistoryPage;
using App.Web.Models.ThirdParty;
using App.Web.Models.Upload;
using App.Web.Models.UserManagement;
using App.Web.Resources;
using dotless.Core.Parser.Infrastructure;
using Microsoft.AspNet.SignalR.Infrastructure;
using Microsoft.WindowsAzure.Storage.Blob;
using Microsoft.WindowsAzure.Storage.RetryPolicies;
using Microsoft.WindowsAzure.Storage.Table;
using BlobRequestOptions = Microsoft.WindowsAzure
.Storage.Blob.BlobRequestOptions;
using StorageException = Microsoft.WindowsAzure
.Storage.StorageException;
1. public virtual ActionResult SetMetadata(int blocksCount, string fileName, long fileSize,
bool fromMimetisme)
2. public virtual async Task<ActionResult> UploadChunk(int id)
3. public virtual async Task<ActionResult> Index()
4. public virtual ActionResult ImportMimetismeRunning(string fileName)
5. public virtual ActionResult ImportMimetisme()
6. public virtual async Task<ActionResult> CloseMonth()
7. public virtual async Task<ActionResult> CloseSelectedMonth(StartImportModel model)
8. public virtual async Task<ActionResult> Correction()
9. public virtual async Task<ActionResult> GetFileNumberOfRows()
10.public virtual async Task<ActionResult> GetMimetismeFileNumberOfRows()
11.public virtual async Task<ActionResult> UploadHistory()
12.public virtual async Task<ActionResult> ClearDatabase()
13.public virtual async Task<ActionResult> UploadHistory()
14.public virtual async Task<ActionResult> ValidateFile(StartImportModel model,
CancellationToken cancellationToken)
15.public virtual async Task<ActionResult> ValidateMimetismeFile(StartImportModel model)
16.public virtual async Task<ActionResult> ValidationReport(StartImportModel model,
CancellationToken cancellationToken)
17.public virtual async Task<ActionResult> ShowReport(string fileToUse)
18.public virtual async Task<ActionResult> ThirdParty([FromUri]int thirdPartyId)
19.public virtual ActionResult Report(string fileToUse)
20.public virtual async Task<ActionResult> HistoriqueImportMimetisme(string fileToUse)
21.public virtual ActionResult MimetismeReport(string fileToUse)
22.public virtual ActionResult GetUploadAside(string fromPage)
23.public virtual ActionResult UploadFromSap()
1. public ITenancyService TenancyService { get; set; }
2. public ITenancyMapping TenancyMapping { get; set; }
3. public IAuthenticationServerService AuthenticationServerService { get; set; }
4. public IThirdPartyDataService ThirdPartyDataService { get; set; }
5. public IFileService ExcelFileService { get; set; }
6. public IUserService UserService { get; set; }
7. public IHistoryService HistoryService { get; set; }
8. public IFileService FileService { get; set; }
9. public IPerimeterCacheService PerimeterCacheService { get; set; }
10.public ISettingsService SettingsService { get; set; }
11.public IForecastService ForecastService { get; set; }
12.public IDatabaseBIConnection DatabaseBIConnection { get; set; }
13.public ITimeLevelService TimeLevelService { get; set; }
14.public IAzureSqlBackupRestoreService AzureSqlBackupRestoreService { get; set; }
15.public IConnectionManager ConnectionManager { get; set; }
16.public IAppLoggerService Log { get; set; }
Проблема
Методы в контроллере Службы
Проблема Решение
Службы Обработчики
Медиатор
Методы в контроллере Команды
Библиотека MediatR
MediatR
public interface Imediator
{
Task Publish<TNotification>(TNotification notification, CancellationToken cancellationToken)
where TNotification : INotification;
Task<TResponse> Send<TResponse>(IRequest<TResponse> request, CancellationToken cancellationToken);
}
public interface IRequest<out TResponse> : IBaseRequest {}
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse>
{
Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken);
}
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse>
{
Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken);
}
public interface INotification {}
public interface INotificationHandler<in TNotification> where TNotification : Inotification
{
Task Handle(TNotification notification, CancellationToken cancellationToken);
}
Демо
Использование MediatR
public async Task<IActionResult> PatchArticle(string articleId,
[FromBody] JsonPatchDocument<ArticleSave> patchDoc)
{
if (ModelState.IsValid)
{
bool PatchAction(Models.Article article) // внутренний метод вместо делегата
{
var saveArticle = Mapper.Map<ArticleSave>(article); // из модели во viewmodel
patchDoc.ApplyTo(saveArticle, ModelState); // применить изменения
if (this.TryValidateModel(saveArticle)) // проверить
{
Mapper.Map(saveArticle, article); // заполнить модель из viewmodel
return true;
}
else
return false;
}
var message = new UpdatePartialCommand<Models.Article>(articleId, PatchAction);
var updatedArticle = await _mediator.Send(message);
var articleViewModel = Mapper.Map<ArticleSave>(updatedArticle);
//sending command to invalidate search index
await _mediator.Send(new InvalidateDocIndexCommand(updatedArticle.TeamId,
updatedArticle.KbId));
return Ok(articleViewModel);
}
else
return this.UnprocessableEntity(ModelState);
}
public struct UpdatePartialCommand<T> : IRequest<T>
{
public readonly string Id;
public readonly Func<T, bool> Patch; // Сюда попадает тот внутренний метод
public UpdatePartialCommand(string id, Func<T, bool> patchAction) { Id = id; Patch = patchAction; }
}
public async Task<Article> Handle(UpdatePartialCommand<Article> command,
CancellationToken cancellationToken)
{
var result = await _articleRepo.FindSingleAsync(command.Id);
if (result == null)
throw new EntityNotFoundException("Model not found"); // Мне не нравится
if (command.Patch(result)) // Здесь вызываем обновление модели
return await _articleRepo.UpdateAsync(command.Id, result);
else
// TODO: embed inner exception
throw new HandlerForbiddenException("Article is not valid"); // Мне не нравится
}
Итоги
 Простой контроллер занимается HTTP
 Команда/запрос и обработчик вместе
 Команды и запросы в разных проектах
 Команда оперирует моделью ОРМ
 Обработчик запроса сразу получает данные в формате
пользователя
 Понятная иерархия: Контроллер/Команда + Обработчик/Службы
 Просто оттестировать
 Проще разбить на микро-службы
Спасибо за внимание
The Secret Circle Solutions
www.scrtcrcl.com
www.fb.com/scrtcrcl/
GitHub
github.com/xperiandri/
Social
www.fb.com/xperiandri
www.vk.com/xperiandri
www.twitter.com/xperiandri
YouTube
www.youtube.com/user/
andriicsharp
Что посмотреть
Jason Taylor
Владимир Хориков
CQRS in practice
Application
Domain
Presentation

Как навести порядок в коде вашего web-приложения, Андрей Чебукин

  • 1.
    Как навести порядокв коде Web-приложения АндрейЧебукин
  • 2.
    Андрей Чебукин (XperiAndri) Azureархитектор DevOps автоматизатор Любитель: F#, Swift, functional programming MCSD:Windows 8 C# Сооснователь:The Secret Circle Solutions Преподаватель: КА ШАГ Студент-партнёр Microsoft: Одесса
  • 3.
    Повестка  К чемуприводит традиционная 3-х уровневая архитектура  CQRS  Медиатор  CQRS + MediatR + AutoMapper = порядок
  • 4.
  • 7.
  • 8.
    И фигнём модельпрямо из контроллера public virtual ActionResult Get(long id) { Binder result = _service.Get(id); return JsonNet(result.Flat((List<object>)null)); }
  • 11.
    И что вышло? Модели БД используются в бизнес-логике  И из чего состоит слой бизнес-логики?
  • 12.
    Мега service public interfaceIBinderService { Binder Get(Guid Id); List<Binder> GetAsync(); Binder Save(Binder binder); Binder Update(Binder task); Binder Create(Binder task); void Delete(Guid id); Section GetSection(long sectionId); }
  • 13.
    Мышление основанное наCRUD  Формы не удобные в использовании  Сложно поддерживаемый, непонятный код с уязвимостями  CRUD anemic model  DTO нас не спасут
  • 14.
    Что можно сделать? ОтдельныеDTO на чтение, создание и изменение
  • 15.
  • 17.
    CQS vs SQRS CommandQuery Separation Метод-команда  Метод-запрос CommandQuery Responsibility Segregation  Модель команды  Модель запроса
  • 18.
    Состав CQRS  Запрос –Retrieve  Команда – Что делать  Событие – Что произошло  Сага
  • 19.
    Команда vs Событие Команда Должна быть выполнена  Обработчик производит действие Событие  Можем реагировать, а можем не реагировать  Обработчик реагирует на действие
  • 20.
    Команда vs DTO Команда Сериализованный вызов метода DTO  Контракт между слоя приложения  Для обратной совметимости
  • 21.
  • 22.
    Может ли командаотправлять команду? Нет
  • 23.
    Сага  Из командыкоманду вызывать нельзя  Нужно сделать сагу, которая будет реализовывать рабочий процесс, который посылает команды
  • 24.
    Варианты реализации  ORM/ADO.NET Optimized indexed views/stored procedures  Full-text search/ElasticSearch
  • 25.
  • 26.
    using System; using System.Collections.Generic; usingSystem.Collections.Immutable; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Web; using System.Web.Http; using System.Web.Mvc; using App.Entities; using App.Models.Upload; using App.Services.Admin.LogIntegrator; using App.Services.AuthenticationServer; using App.Services.DatabaseManagement; using App.Services.DataImport; using App.Services.DataImport.BO; using App.Services.DataImport.Dal; using App.Services.Forecast; using App.Services.Forecast.Bo; using App.Services.Helpers; using App.Services.History; using App.Services.Logging; using App.Services.MultiTenancy; using App.Services.Perimeters; using App.Services.Settings; using App.Services.ThirdParty; using App.Services.ThirdParty.Bo; using App.Services.TimeLevel; using App.Services.User; using App.Web.Filters; using App.Web.Helpers; using App.Web.Hubs; using App.Web.Models.HistoryPage; using App.Web.Models.ThirdParty; using App.Web.Models.Upload; using App.Web.Models.UserManagement; using App.Web.Resources; using dotless.Core.Parser.Infrastructure; using Microsoft.AspNet.SignalR.Infrastructure; using Microsoft.WindowsAzure.Storage.Blob; using Microsoft.WindowsAzure.Storage.RetryPolicies; using Microsoft.WindowsAzure.Storage.Table; using BlobRequestOptions = Microsoft.WindowsAzure .Storage.Blob.BlobRequestOptions; using StorageException = Microsoft.WindowsAzure .Storage.StorageException;
  • 27.
    1. public virtualActionResult SetMetadata(int blocksCount, string fileName, long fileSize, bool fromMimetisme) 2. public virtual async Task<ActionResult> UploadChunk(int id) 3. public virtual async Task<ActionResult> Index() 4. public virtual ActionResult ImportMimetismeRunning(string fileName) 5. public virtual ActionResult ImportMimetisme() 6. public virtual async Task<ActionResult> CloseMonth() 7. public virtual async Task<ActionResult> CloseSelectedMonth(StartImportModel model) 8. public virtual async Task<ActionResult> Correction() 9. public virtual async Task<ActionResult> GetFileNumberOfRows() 10.public virtual async Task<ActionResult> GetMimetismeFileNumberOfRows() 11.public virtual async Task<ActionResult> UploadHistory() 12.public virtual async Task<ActionResult> ClearDatabase() 13.public virtual async Task<ActionResult> UploadHistory() 14.public virtual async Task<ActionResult> ValidateFile(StartImportModel model, CancellationToken cancellationToken) 15.public virtual async Task<ActionResult> ValidateMimetismeFile(StartImportModel model) 16.public virtual async Task<ActionResult> ValidationReport(StartImportModel model, CancellationToken cancellationToken) 17.public virtual async Task<ActionResult> ShowReport(string fileToUse) 18.public virtual async Task<ActionResult> ThirdParty([FromUri]int thirdPartyId) 19.public virtual ActionResult Report(string fileToUse) 20.public virtual async Task<ActionResult> HistoriqueImportMimetisme(string fileToUse) 21.public virtual ActionResult MimetismeReport(string fileToUse) 22.public virtual ActionResult GetUploadAside(string fromPage) 23.public virtual ActionResult UploadFromSap()
  • 28.
    1. public ITenancyServiceTenancyService { get; set; } 2. public ITenancyMapping TenancyMapping { get; set; } 3. public IAuthenticationServerService AuthenticationServerService { get; set; } 4. public IThirdPartyDataService ThirdPartyDataService { get; set; } 5. public IFileService ExcelFileService { get; set; } 6. public IUserService UserService { get; set; } 7. public IHistoryService HistoryService { get; set; } 8. public IFileService FileService { get; set; } 9. public IPerimeterCacheService PerimeterCacheService { get; set; } 10.public ISettingsService SettingsService { get; set; } 11.public IForecastService ForecastService { get; set; } 12.public IDatabaseBIConnection DatabaseBIConnection { get; set; } 13.public ITimeLevelService TimeLevelService { get; set; } 14.public IAzureSqlBackupRestoreService AzureSqlBackupRestoreService { get; set; } 15.public IConnectionManager ConnectionManager { get; set; } 16.public IAppLoggerService Log { get; set; }
  • 29.
  • 31.
  • 32.
  • 33.
    MediatR public interface Imediator { TaskPublish<TNotification>(TNotification notification, CancellationToken cancellationToken) where TNotification : INotification; Task<TResponse> Send<TResponse>(IRequest<TResponse> request, CancellationToken cancellationToken); } public interface IRequest<out TResponse> : IBaseRequest {} public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse> { Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken); } public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse> { Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken); } public interface INotification {} public interface INotificationHandler<in TNotification> where TNotification : Inotification { Task Handle(TNotification notification, CancellationToken cancellationToken); }
  • 34.
  • 35.
    public async Task<IActionResult>PatchArticle(string articleId, [FromBody] JsonPatchDocument<ArticleSave> patchDoc) { if (ModelState.IsValid) { bool PatchAction(Models.Article article) // внутренний метод вместо делегата { var saveArticle = Mapper.Map<ArticleSave>(article); // из модели во viewmodel patchDoc.ApplyTo(saveArticle, ModelState); // применить изменения if (this.TryValidateModel(saveArticle)) // проверить { Mapper.Map(saveArticle, article); // заполнить модель из viewmodel return true; } else return false; } var message = new UpdatePartialCommand<Models.Article>(articleId, PatchAction); var updatedArticle = await _mediator.Send(message); var articleViewModel = Mapper.Map<ArticleSave>(updatedArticle); //sending command to invalidate search index await _mediator.Send(new InvalidateDocIndexCommand(updatedArticle.TeamId, updatedArticle.KbId)); return Ok(articleViewModel); } else return this.UnprocessableEntity(ModelState); }
  • 36.
    public struct UpdatePartialCommand<T>: IRequest<T> { public readonly string Id; public readonly Func<T, bool> Patch; // Сюда попадает тот внутренний метод public UpdatePartialCommand(string id, Func<T, bool> patchAction) { Id = id; Patch = patchAction; } } public async Task<Article> Handle(UpdatePartialCommand<Article> command, CancellationToken cancellationToken) { var result = await _articleRepo.FindSingleAsync(command.Id); if (result == null) throw new EntityNotFoundException("Model not found"); // Мне не нравится if (command.Patch(result)) // Здесь вызываем обновление модели return await _articleRepo.UpdateAsync(command.Id, result); else // TODO: embed inner exception throw new HandlerForbiddenException("Article is not valid"); // Мне не нравится }
  • 37.
    Итоги  Простой контроллерзанимается HTTP  Команда/запрос и обработчик вместе  Команды и запросы в разных проектах  Команда оперирует моделью ОРМ  Обработчик запроса сразу получает данные в формате пользователя  Понятная иерархия: Контроллер/Команда + Обработчик/Службы  Просто оттестировать  Проще разбить на микро-службы
  • 38.
    Спасибо за внимание TheSecret Circle Solutions www.scrtcrcl.com www.fb.com/scrtcrcl/ GitHub github.com/xperiandri/ Social www.fb.com/xperiandri www.vk.com/xperiandri www.twitter.com/xperiandri YouTube www.youtube.com/user/ andriicsharp
  • 39.
  • 40.
  • 41.
  • 43.

Editor's Notes

  • #14 Логика растекается в сервисы
  • #18 CQS – Bertrand Meyer. Французский академик. Язык Eiffel CQRS – Grag Young 2010 https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf
  • #43 Onion по DDD