В прошлой статье я привел пример фабрики для получения реализаций 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 может быть передана в слой представления/сериализована безболезненно «как есть».