В прошлой статье я привел пример фабрики для получения реализаций IQuery, но не объяснил механизм ее работы
В данном материале я хочу поделиться техникой регистрации необходимых компонентов сборки по соглашениям. Сейчас у меня под рукой кодовая база с другой реализацией CQRS, поэтому примеры будут отличаться. Это не принципиально: основная идея остается неизменной.
Допустим у вас есть такой интерфейс, где ListParams – спецификация, приходящая с фронтенда
Задача
Избавить прикладных разработчиков от необходимости написания контроллеров, проекций и сервисов.
Решение
Создадим базовый класс для операции List:
Метод ProjectTo – это фишка AutoMapper, позволяющая строить проекции по соглашениям. Избавляет от необходимости поднимать в память всю Entity, при этом позволяя не писать унылые конструкции Select вида
Виртуальные методы AddEntityBusinessLogic и AddProjectionBusinessLogic позволяют добавить условия фильтрации до и после создания проекции.
Теперь для быстрого прототипирования мы можем использовать ListOperationBase<TEntity, TDto> а для настоящих реализаций потребуется создать настоящие операции с правильной логикой. Для этого на старте приложение нужно зарегистрировать все, что есть в сборке по соглашениям. В моем случае используется модульная архитектура и это код загрузки модуля. Для монолитных приложений потребуется еще составить список сборок, из которых вы хотите загрузить типы.
Вам потребуется всего один контроллер для всех Crud операций. Реализацию ControllerSelector’а для Generic WebApi контроллеров вы можете найти по ссылке: github.com/hightechtoday/costeffectivecode/blob/master/src/CostEffectiveCode.WebApi2/WebApi/Infrastructure/RuntimeScaffoldingHttpControllerSelector.cs
Передача контейнера в контроллер конечно идея так себе (ServiceLocator) и на самом деле гораздо лучше обернуть вызов в фабричный метод (как сделано в примере с QueryFactory). Еще одно слабое место – что делать если зарегистрировано 2 реализации IListOperation с одинаковыми типами. На этот вопрос нет однозначного ответа: все зависит от специфики вашего приложения и требований к системе
В итоге мы получили систему для быстрого прототипирования, избавляющую программиста от написания контроллеров и регистрации сервисов в контейнере. Все что необходимо сделать – добавить сущность, DTO и описать маппинг. В случае использования AutoMapper однозначно следует добавить конструкцию Mapper.AssertConfigurationIsValid(); Она поможет узнать об ошибках, если придется изменить Entity или Dto. Кстати, по аналогии с регистрации операций можно автоматизировать и создание маппингов по соглашениям для случаев, когда все маппинги очевидны. Однако в реальной жизни дописывать несколько строчек к маппингу приходится довольно часто, поэтому я предпочитаю делать это вручную, благо это всего пара строчек.
По шагам
Маппинг можно опустить, если Entity может быть передана в слой представления/сериализована безболезненно «как есть».
_queryFactory.GetQuery<Product>()
.Where(Product.ActiveRule)
.OrderBy(x => x.Id)
.Paged(0, 10) // получаем 10 продуктов для первой страницы
// Мы решили подключить полнотекстовый поиск и добавили ElasticSearch, не вопрос:
_queryFactory.GetQuery<Product, FullTextSpecification>()
.Where(new FullTextSpecification(«зонтик»))
.All()
// Или EF тормозит и мы решили переделать на хранимую процедуру и Dapper
_queryFactory.GetQuery<Product, DictionarySpecification, DapperQuery>()
.Where(new DictionarySpecification (someDirctionary))
.All()
В данном материале я хочу поделиться техникой регистрации необходимых компонентов сборки по соглашениям. Сейчас у меня под рукой кодовая база с другой реализацией CQRS, поэтому примеры будут отличаться. Это не принципиально: основная идея остается неизменной.
Допустим у вас есть такой интерфейс, где ListParams – спецификация, приходящая с фронтенда
public interface IListOperation<TDto>
{
ListResult<TDto> List(ListParams listParam);
}
Задача
Избавить прикладных разработчиков от необходимости написания контроллеров, проекций и сервисов.
Решение
Создадим базовый класс для операции List:
public class ListOperationBase<TEntity, TDto> : IListOperation<TDto>
where TEntity: IEntity
where TDto: IHaveId
{
protected readonly IDbContext DbContext ;
public ListOperationBase(IDbContext dbContext )
{
if (dbContext == null) throw new ArgumentNullException(nameof(dbContext));
DbContext = dataStore;
}
public virtual ListResult<TDto> List(ListParam listParam)
{
var data = AddProjectionBusinessLogic(AddEntityBusinessLogic(DataStore
.GetAll<TEntity>())
.ProjectTo<TDto>())
.Filter(listParam);
return new ListResult<TDto>()
{
Data = data
.Paging(listParam)
.ToList(),
TotalCount = data.Count()
};
}
protected virtual IQueryable<TEntity> AddEntityBusinessLogic(IQueryable<TEntity> queryable) => queryable;
protected virtual IQueryable<TDto> AddProjectionBusinessLogic(IQueryable<TDto> queryable) => queryable;
}
Метод ProjectTo – это фишка AutoMapper, позволяющая строить проекции по соглашениям. Избавляет от необходимости поднимать в память всю Entity, при этом позволяя не писать унылые конструкции Select вида
Query.Select(x => {
Name = x.Name,
ParentUrl = x.Parent.Url,
Foo = x.Foo
})
Виртуальные методы AddEntityBusinessLogic и AddProjectionBusinessLogic позволяют добавить условия фильтрации до и после создания проекции.
Теперь для быстрого прототипирования мы можем использовать ListOperationBase<TEntity, TDto> а для настоящих реализаций потребуется создать настоящие операции с правильной логикой. Для этого на старте приложение нужно зарегистрировать все, что есть в сборке по соглашениям. В моем случае используется модульная архитектура и это код загрузки модуля. Для монолитных приложений потребуется еще составить список сборок, из которых вы хотите загрузить типы.
var types = GetType().Assembly.GetTypes();
var operations = types
.Where(t.IsClass
&& !t.IsAbstract
&& t.ImplementsOpenGenericInterface(typeof(IListOperation<>)));
foreach (var operation in operations)
{
var definitions =
operation.GetInterfaces().Where(i => i.ImplementsOpenGenericInterface(typeof (IListOperation<>)));
foreach (var definition in definitions)
{
Container.Register(definition, operation);
}
// ...
}
Вам потребуется всего один контроллер для всех Crud операций. Реализацию ControllerSelector’а для Generic WebApi контроллеров вы можете найти по ссылке: github.com/hightechtoday/costeffectivecode/blob/master/src/CostEffectiveCode.WebApi2/WebApi/Infrastructure/RuntimeScaffoldingHttpControllerSelector.cs
public ListResult<TListDto> List(ListParam loadParams) =>
(_container.ResolveAll<IListOperation<TListDto>>().SingleOrDefault() ?? new ListOperationBase<TEntity,TListDto>(DataStore))
.List(loadParams);
Передача контейнера в контроллер конечно идея так себе (ServiceLocator) и на самом деле гораздо лучше обернуть вызов в фабричный метод (как сделано в примере с QueryFactory). Еще одно слабое место – что делать если зарегистрировано 2 реализации IListOperation с одинаковыми типами. На этот вопрос нет однозначного ответа: все зависит от специфики вашего приложения и требований к системе
В итоге мы получили систему для быстрого прототипирования, избавляющую программиста от написания контроллеров и регистрации сервисов в контейнере. Все что необходимо сделать – добавить сущность, DTO и описать маппинг. В случае использования AutoMapper однозначно следует добавить конструкцию Mapper.AssertConfigurationIsValid(); Она поможет узнать об ошибках, если придется изменить Entity или Dto. Кстати, по аналогии с регистрации операций можно автоматизировать и создание маппингов по соглашениям для случаев, когда все маппинги очевидны. Однако в реальной жизни дописывать несколько строчек к маппингу приходится довольно часто, поэтому я предпочитаю делать это вручную, благо это всего пара строчек.
По шагам
- Добавляем SomeEntity: IEntity
- Добавляем SomeEntityListDto
- Регистрируем маппинг SomeEntity -> SomeEntityListDto
- Автоматом получаем метод /SomeEntity/List
- Дописываем бизнес-логику в SomeEntityListOperation<SomeEntity, SomeEntityListDto>
- Метод /SomeEntity/List начинает использовать новую реализацию с «правильной» бизнес-логикой
Маппинг можно опустить, если Entity может быть передана в слой представления/сериализована безболезненно «как есть».
Поделиться с друзьями
mihasic
итого:
а потом:
возникает вопрос — зачем такая сложность (accidental complexity)?
SomeEntityListOperation — абстракция от WebApi контроллера:
Касательно компонентов в других сборках — уже также есть довольно популярное решение: middleware (например, специфиципровано в owin и реализовано в katana и, кстати, поддерживает WebApi). Само использование таких компонентов позволяем избавиться от дополнительных зависимостей между ними в приложении (разве что фрэймворк которых и так идет "из коробки").
marshinov
Вы привели множество дельных советов, к сожалению, не относящихся к теме материала. CQRS как архитектурный паттерн — это в первую очередь про абстрагирование бизнес-логики от инфраструктурного слоя и структурирование кода. Операции (комманды и квейри) могут быть выполнены за пределами WebApi (WCF, ServiceStack, Akka.NET,...) и Web-контекста в целом (например, отправлены в очередь). Кроме этого возможно переиспользование операций друг другом (опять же на уровне бизнес-логики).
Есть еще много причин, почему я не хочу хранить бизнес-логику в теле методов контроллеров (пусть и WebApi), но это очень длинная тема и я не хочу ее развивать.
mihasic
с чего вы это взяли? [http://martinfowler.com/bliki/CQRS.html]
могут, но в момент написания кода мы уже знаем контекст в котором будем выполнять команду или запрос. и код будет сильно отличаться в зависимости от контекста:
никто не может запретить вам этого. я веду к тому что контекст — важная часть при принятии архитектурных решений от которой никуда не деться. не нравиться хранить код в завязанных на фрэймворк классах — выделите в отдельный чистый и вызывайте оттуда (хотя мое правило — только при необходимисти, как переиспользование функционала либо когда у кода разная ответственность). а излишние абстракции только усложняют код и вносят ограничения на реализацию возможностей инфраструктуры.
mihasic
кстати, немного по теме DDD, SQRS, ES. есть англоязычная чат группа на slack, в основном так люди с бэкграундом .net. довольно многие если что ответят/подскажут.
началось так: https://twitter.com/randompunter/status/681830829203533824
marshinov
Этот подход хорошо работает в продуктовой разработке, когда вы работаете долго только со своей кодовой базой. Мы работаем в аутсорсе, часто с кодовой базой сомнительного качества. Если есть IOC, мы просто вставляем свой repair-toolkit и начинаем постепенно, вместе с разработкой новых фич, вырезать плохо-пахнущие участки. В этом случае абстрагирование фасадом от внешней инфраструктуры нам помогает. Каков поп, таков и приход, не находите?