Рентабельный код 3: Немного особой контейнерной магии

в 9:00, , рубрики: .net, conventions > configuration, ioc/di, Программирование, Проектирование и рефакторинг, Разработка веб-сайтов

В прошлой статье я привел пример фабрики для получения реализаций IQuery, но не объяснил механизм ее работы
Рентабельный код 3: Немного особой контейнерной магии - 1

_queryFactory.GetQuery<Product>()
    .Where(Product.ActiveRule) // это статический экспрешн, как в примере с Account. Используется ExpressionSpecification
    .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. Кстати, по аналогии с регистрации операций можно автоматизировать и создание маппингов по соглашениям для случаев, когда все маппинги очевидны. Однако в реальной жизни дописывать несколько строчек к маппингу приходится довольно часто, поэтому я предпочитаю делать это вручную, благо это всего пара строчек.

По шагам

  1. Добавляем SomeEntity: IEntity
  2. Добавляем SomeEntityListDto
  3. Регистрируем маппинг SomeEntity -> SomeEntityListDto
  4. Автоматом получаем метод /SomeEntity/List
  5. Дописываем бизнес-логику в SomeEntityListOperation<SomeEntity, SomeEntityListDto>
  6. Метод /SomeEntity/List начинает использовать новую реализацию с «правильной» бизнес-логикой

Автор: marshinov

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js