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