Немного особой контейнерной магии

    В прошлой статье я привел пример фабрики для получения реализаций IQuery, но не объяснил механизм ее работы
    _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. Кстати, по аналогии с регистрации операций можно автоматизировать и создание маппингов по соглашениям для случаев, когда все маппинги очевидны. Однако в реальной жизни дописывать несколько строчек к маппингу приходится довольно часто, поэтому я предпочитаю делать это вручную, благо это всего пара строчек.

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

    Маппинг можно опустить, если Entity может быть передана в слой представления/сериализована безболезненно «как есть».
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 5
    • 0

      итого:


      Задача
      Избавить прикладных разработчиков от необходимости написания контроллеров, проекций и сервисов.

      а потом:


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

      возникает вопрос — зачем такая сложность (accidental complexity)?


      SomeEntityListOperation — абстракция от WebApi контроллера:


      • url операции может быть объявлен через аттрибут
      • задачу DI зависимостей в контроллер уже как бы решили
      • автоматом получаем content negotiation
      • нет необходимости внедрения костылей при требовании кэширования и т.п.: ETag, Expiration, HEAD request etc.
      • авторизация тоже поддерживается и настраивается по разным операциям
      • нет необходимости сканировать все типы сборки(ок)

      Касательно компонентов в других сборках — уже также есть довольно популярное решение: middleware (например, специфиципровано в owin и реализовано в katana и, кстати, поддерживает WebApi). Само использование таких компонентов позволяем избавиться от дополнительных зависимостей между ними в приложении (разве что фрэймворк которых и так идет "из коробки").

      • 0
        Вы привели множество дельных советов, к сожалению, не относящихся к теме материала. CQRS как архитектурный паттерн — это в первую очередь про абстрагирование бизнес-логики от инфраструктурного слоя и структурирование кода. Операции (комманды и квейри) могут быть выполнены за пределами WebApi (WCF, ServiceStack, Akka.NET,...) и Web-контекста в целом (например, отправлены в очередь). Кроме этого возможно переиспользование операций друг другом (опять же на уровне бизнес-логики).
        Есть еще много причин, почему я не хочу хранить бизнес-логику в теле методов контроллеров (пусть и WebApi), но это очень длинная тема и я не хочу ее развивать.
        • 0
          CQRS как архитектурный паттерн — это в первую очередь про абстрагирование бизнес-логики от инфраструктурного слоя и структурирование кода

          с чего вы это взяли? [http://martinfowler.com/bliki/CQRS.html]


          Операции (комманды и квейри) могут быть выполнены за пределами WebApi (WCF, ServiceStack, Akka.NET,...) и Web-контекста в целом (например, отправлены в очередь).

          могут, но в момент написания кода мы уже знаем контекст в котором будем выполнять команду или запрос. и код будет сильно отличаться в зависимости от контекста:


          • для HTTP (WebApi. NancyFX, ServiceStack...) всегда можно использовать специфичные техники (ETag, client cache...)
          • для actor-framework (akka, orleans), это специфичный только там код
          • WCF — с моей точки зрения отдельная тема (я бы не использовал технологию, пока не прийдется — например, интеграция с особенными протоколами)
          • Service Bus — как и с перечисленным выше, это такой же контекст, архитектурное решения, которое влияет на весь остальной код

          Есть еще много причин, почему я не хочу хранить бизнес-логику в теле методов контроллеров (пусть и WebApi), но это очень длинная тема и я не хочу ее развивать.

          никто не может запретить вам этого. я веду к тому что контекст — важная часть при принятии архитектурных решений от которой никуда не деться. не нравиться хранить код в завязанных на фрэймворк классах — выделите в отдельный чистый и вызывайте оттуда (хотя мое правило — только при необходимисти, как переиспользование функционала либо когда у кода разная ответственность). а излишние абстракции только усложняют код и вносят ограничения на реализацию возможностей инфраструктуры.

          • +1

            кстати, немного по теме DDD, SQRS, ES. есть англоязычная чат группа на slack, в основном так люди с бэкграундом .net. довольно многие если что ответят/подскажут.
            началось так: https://twitter.com/randompunter/status/681830829203533824

            • 0
              (хотя мое правило — только при необходимисти, как переиспользование функционала либо когда у кода разная ответственность). а излишние абстракции только усложняют код и вносят ограничения на реализацию возможностей инфраструктуры.

              Этот подход хорошо работает в продуктовой разработке, когда вы работаете долго только со своей кодовой базой. Мы работаем в аутсорсе, часто с кодовой базой сомнительного качества. Если есть IOC, мы просто вставляем свой repair-toolkit и начинаем постепенно, вместе с разработкой новых фич, вырезать плохо-пахнущие участки. В этом случае абстрагирование фасадом от внешней инфраструктуры нам помогает. Каков поп, таков и приход, не находите?

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.