About decorators, cross-cutting functionality, CQRS, and layered architecture

    The developer of SimpleInjector is very fond of "decorators" , especially in combination with generic look . This approach allows you to "hang" on handlers what are commonly called cross-cutting concerns without registering and SMS interception and special street magic like Fody or PostSharp . CQRS is not a top level architecture, so I want to have the same decorators for the classic Application Service. Under the cut, I’ll tell you how to do it.
    QueryHandler, CommandHanler





    What is cross-cutting concern?


    Cross-cutting concern is a term from AOP. The “auxiliary” functionality of the module is related to cross-cutting, not directly related to the task being performed, but necessary, for example:
    • synchronization
    • error processing
    • validation
    • transaction management
    • caching
    • logging
    • monitoring

    This logic is usually difficult to separate from the core. Check out the two examples below.

    Code without cross-cutting concern


    public Book GetBook(int bookId)
      => dbContext.Books.FirstorDefault(x => x.Id == bookId);
     

    Cross-cutting concern code


    public Book GetBook(int bookId)
    {
      if (!SecurityContext.GetUser().HasRight("GetBook"))
        throw new AuthException("Permission Denied");
      Log.debug("Call method GetBook with id " + bookId);
      Book book = null;
      String cacheKey = "getBook:" + bookId;
      try
      {
        if (cache.contains(cacheKey))
        {
          book = cache.Get(cacheKey);
        }
        else
        {
          book = dbContext.Books.FirstorDefault(x => x.Id == bookId);
          cache.Put(cacheKey, book);
        }
      }
      catch(SqlException e)
      {
        throw new ServiceException(e);
      }
      Log.Debug("Book info is: " + book.toString());
      return book;
     }
    }

    Instead of one line it turned out more than twenty. And most importantly, this code will have to be repeated again and again. Decorators come to the rescue .
    Decorator (Eng. Decorator) - a structural design template designed to dynamically connect additional behavior to the object. The Decorator pattern provides a flexible alternative to the practice of subclassing to extend functionality.

    Decorators in CQRS


    For example, you want to enable global validation. It is enough to declare such a decorator:

    public class ValidationCommandHandlerDecorator : ICommandHandler
    {
        private readonly IValidator validator;
        private readonly ICommandHandler decoratee;
        public ValidationCommandHandlerDecorator(IValidator validator,
            ICommandHandler decoratee)
       {
            this.validator = validator;
            this.decoratee = decoratee;
        }
        void ICommandHandler.Handle(TCommand command)
        {
            // validate the supplied command (throws when invalid).
            this.validator.ValidateObject(command);
            // forward the (valid) command to the real command handler.
            this.decoratee.Handle(command);
        }
    }

    And register it for all command handlers:

    container.RegisterDecorator(
        typeof(ICommandHandler<>),
        typeof(ValidationCommandHandlerDecorator<>));


    Now, for all implementations of the interface, ICommandHandlervalidation will occur in the decorator, and the code of the handlers will remain simple.
    public interface ICommandHandler
    {
        TOutput Handle(TInput command);
    }
    public class AddBookCommandHandler: ICommandHandler
    {
        public bool Handle(BookDto dto)
        {
             var entity = Mapper.Map(dto);
             dbContext.Books.Add(entity);
             dbContext.SaveChanges();
             return entity.Id;
        }
    }
    

    But then you have to write on a set of decorators for ICommandHandlerand IQueryHandler. You can of course work around this problem with the help of delegates . But it turns out not very nice and applies only to CQRS, i.e. only in some separate bounded context of the application where CQRS is justified .

    Difference IHandlerandApplication Service


    The main problem with the global use of decorators for the service layer is that service interfaces are more complex than generic handlers. If all the handlers implement this generic interface:

    public interface ICommandHandler
    {
        TOutput Handle(TInput command);
    }

    That services usually implement one method for each use case

    public interface IAppService
    {
        ResponseType UseCase1(RequestType1 request);
        ResponseType UseCase2(RequestType2 request);
        ResponseType UseCase3(RequestType3 request);
        //...
        ResponseType UseCaseN(RequestTypeN request);
    }

    You can’t use an abstract decorator with validation anymore; you have to write a decorator for each service, which kills the very idea of ​​writing a validation code once and forget about it. Moreover, then it is easier to write validation code inside the methods than decorating them.

    Mediatr


    For CQRS, you can solve the problem of duplicating decorators by introducing an interface IRequestHandlerand using it for Commandand Query. The division into reading and writing subsystems in this case lies with naming conventions. SomeCommandRequestHandler: IRequestHandler- obviously, a command handler, and SomeQueryRequestHandler: IRequestHandler- requests. this approach is implemented in MediatR . As an alternative to decorators, the library provides a behaviors mechanism .

    IRequestHandlerIUseCaseHandler


    Why not rename the interface IRequestHandlerto IUseCaseHandler. Request handlers and commands are holistic abstractions , so each of them handles the use case as a whole. Then you can rewrite the CQRS architecture as follows:

    public interface IUseCaseHandler
    {
        TOutput Handle(TInput command);
    }
    public interface IQueryHandler
        : IUseCaseHandler
        where TInput: IQuery
    {
    }
    public interface ICommandHandler
        : IUseCaseHandler
        where TInput: ICommand
    {
    }

    Now "general" decorators can be hung on IUseCaseHandler. At the same time, write decorators for ICommandHandlerand IQueryHandler, for example, for independent transaction management.

    Decorators for Application Service


    IUseCaseHandlerWe can also use the interface in Application Services if we use an explicit implementation .

    public class AppService
        : IAppService
        : IUseCaseHandler
        : IUseCaseHandler
        : IUseCaseHandler
        //...
        : IUseCaseHandler
    {
        public ResponseType1 UseCase1(RequestType1 request) 
        {
            //...
        }
        IUseCaseHandler.Handle(RequestType1 request)
            => UseCase1(request);
        //...
        ResponseTypeN UseCaseN(RequestTypeN request)
        {
            //...
        }
        IUseCaseHandler.Handle(RequestTypeN request)
            => UseCaseN(request);
        //...
    }

    It is necessary to use interfaces in the application code IUseCaseHandler, but not IAppServicebecause decorators will be applied only to the generic interface.

    Error processing


    Let's go back to the validation example. The validator in the code below throws an exception when an invalid command is received. Using exceptions to handle user input is a debatable issue .

    void ICommandHandler.Handle(TCommand command)
    {
        // validate the supplied command (throws when invalid).
        this.validator.ValidateObject(command);
        // forward the (valid) command to the real command handler.
        this.decoratee.Handle(command);
    }

    If you prefer to explicitly indicate in the method signature that execution may fail, the example above can be rewritten like this:

    Result ICommandHandler.Handle(TCommand command)
    {
        return this.validator.ValidateObject(command) && this.decoratee.Handle(command);
    }

    Thus, it will be possible to further separate the decorators by the type of the return value. For example, to log methods that return Resultdifferently than methods that return untracked values.

    Also popular now: