Рентабельный код 3: Немного особой контейнерной магии
_queryFactory.GetQuery()
.Where(Product.ActiveRule)
.OrderBy(x => x.Id)
.Paged(0, 10) // получаем 10 продуктов для первой страницы
// Мы решили подключить полнотекстовый поиск и добавили ElasticSearch, не вопрос:
_queryFactory.GetQuery()
.Where(new FullTextSpecification(«зонтик»))
.All()
// Или EF тормозит и мы решили переделать на хранимую процедуру и Dapper
_queryFactory.GetQuery()
.Where(new DictionarySpecification (someDirctionary))
.All()
В данном материале я хочу поделиться техникой регистрации необходимых компонентов сборки по соглашениям. Сейчас у меня под рукой кодовая база с другой реализацией CQRS, поэтому примеры будут отличаться. Это не принципиально: основная идея остается неизменной.
Допустим у вас есть такой интерфейс, где ListParams — спецификация, приходящая с фронтенда
public interface IListOperation
{
ListResult List(ListParams listParam);
}
Задача
Избавить прикладных разработчиков от необходимости написания контроллеров, проекций и сервисов.
Решение
Создадим базовый класс для операции List:
public class ListOperationBase : IListOperation
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 List(ListParam listParam)
{
var data = AddProjectionBusinessLogic(AddEntityBusinessLogic(DataStore
.GetAll())
.ProjectTo())
.Filter(listParam);
return new ListResult()
{
Data = data
.Paging(listParam)
.ToList(),
TotalCount = data.Count()
};
}
protected virtual IQueryable AddEntityBusinessLogic(IQueryable queryable) => queryable;
protected virtual IQueryable AddProjectionBusinessLogic(IQueryable queryable) => queryable;
}
Метод ProjectTo — это фишка AutoMapper, позволяющая строить проекции по соглашениям. Избавляет от необходимости поднимать в память всю Entity, при этом позволяя не писать унылые конструкции Select вида
Query.Select(x => {
Name = x.Name,
ParentUrl = x.Parent.Url,
Foo = x.Foo
})
Виртуальные методы AddEntityBusinessLogic и AddProjectionBusinessLogic позволяют добавить условия фильтрации до и после создания проекции.
Теперь для быстрого прототипирования мы можем использовать ListOperationBase
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 List(ListParam loadParams) =>
(_container.ResolveAll>().SingleOrDefault() ?? new ListOperationBase(DataStore))
.List(loadParams);
Передача контейнера в контроллер конечно идея так себе (ServiceLocator) и на самом деле гораздо лучше обернуть вызов в фабричный метод (как сделано в примере с QueryFactory). Еще одно слабое место — что делать если зарегистрировано 2 реализации IListOperation с одинаковыми типами. На этот вопрос нет однозначного ответа: все зависит от специфики вашего приложения и требований к системе
В итоге мы получили систему для быстрого прототипирования, избавляющую программиста от написания контроллеров и регистрации сервисов в контейнере. Все что необходимо сделать — добавить сущность, DTO и описать маппинг. В случае использования AutoMapper однозначно следует добавить конструкцию Mapper.AssertConfigurationIsValid (); Она поможет узнать об ошибках, если придется изменить Entity или Dto. Кстати, по аналогии с регистрации операций можно автоматизировать и создание маппингов по соглашениям для случаев, когда все маппинги очевидны. Однако в реальной жизни дописывать несколько строчек к маппингу приходится довольно часто, поэтому я предпочитаю делать это вручную, благо это всего пара строчек.
По шагам
- Добавляем SomeEntity: IEntity
- Добавляем SomeEntityListDto
- Регистрируем маппинг SomeEntity → SomeEntityListDto
- Автоматом получаем метод /SomeEntity/List
- Дописываем бизнес-логику в SomeEntityListOperation
- Метод /SomeEntity/List начинает использовать новую реализацию с «правильной» бизнес-логикой