, CommandHanler Source: https://habr.com/ru/post/353258/About decorators, end-to-end functionality, CQRS and layered architecture
Developer SimpleInjector loves "decorators" very much , especially when combined with generics
QueryHandler<TIn, TOut> , CommandHanler<TIn, TOut>
.
This approach allows you to “hang” on the handlers what is commonly called cross-cutting concerns without registration 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.What is cross-cutting concern?
Cross-cutting concern is a term from AOP. The “auxiliary” functionality of the module that is not directly related to the task being performed, but necessary, for example, is related to end-to-end, for example:
This logic is usually difficult to separate from the main one. Note the two examples below.
')
Code without cross-cutting concern
public Book GetBook(int bookId) => dbContext.Books.FirstorDefault(x => x.Id == bookId);
Code with cross-cutting concern
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<Book>(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 (English Decorator) is a structural design pattern designed to dynamically connect additional behavior to an object. The Decorator pattern provides a flexible alternative to the practice of creating subclasses to extend functionality.
Decorators in CQRS
For example, I want to enable global validation. It is enough to declare this decorator:
public class ValidationCommandHandlerDecorator<TCommand> : ICommandHandler<TCommand> { private readonly IValidator validator; private readonly ICommandHandler<TCommand> decoratee; public ValidationCommandHandlerDecorator(IValidator validator, ICommandHandler<TCommand> decoratee) { this.validator = validator; this.decoratee = decoratee; } void ICommandHandler<TCommand>.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 ICommandHandler
interface, validation will occur in the decorator, and the code of the handlers will remain simple.
public interface ICommandHandler<in TInput, out TOutput> { TOutput Handle(TInput command); } public class AddBookCommandHandler: ICommandHandler<BookDto, int> { public bool Handle(BookDto dto) { var entity = Mapper.Map<Book>(dto); dbContext.Books.Add(entity); dbContext.SaveChanges(); return entity.Id; } }
But then you have to write on a set of decorators for ICommandHandler
and IQueryHandler
. You can certainly get around this problem with the help of delegates . But it turns out not very nice and is applicable only to CQRS, i.e. only in a separate bounded context of the application where CQRS is justified .Difference
IHandler
and Application Service
The main problem with the global use of decorators for the service layer is that service interfaces are more complicated than generic handlers. If all handlers implement this generic interface:
public interface ICommandHandler<in TInput, out TOutput> { 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); }
An abstract decorator with validation is no longer applicable; you will have to write on the decorator for each service, which kills the very idea of ​​writing the validation code once and forgetting about it. Moreover, it is easier then to write validation code inside methods than to decorate them.MediatR
For CQRS, you can solve the problem of duplication of decorators, if you enter the interface IRequestHandler
and use it for Command
and Query
. The division into reading and writing subsystems in this case falls on naming conventions. SomeCommandRequestHandler: IRequestHandler
is obviously a command handler, and SomeQueryRequestHandler: IRequestHandler
is a query. This approach is implemented in MediatR . As an alternative to decorators, the library provides a mechanism for behaviors .IRequestHandler
→ IUseCaseHandler
Why not rename the IRequestHandler
interface to IUseCaseHandler
. Request handlers and commands are holistic abstractions , so each of them handles the use case entirely. Then you can rewrite the CQRS architecture as follows:
public interface IUseCaseHandler<in TInput, out TOutput> { TOutput Handle(TInput command); } public interface IQueryHandler<in TInput, out TOutput> : IUseCaseHandler<in TInput, out TOutput> where TInput: IQuery<TOutput> { } public interface ICommandHandler<in TInput, out TOutput> : IUseCaseHandler<in TInput, out TOutput> where TInput: ICommand<TOutput> { }
Now “common” decorators can be hung on IUseCaseHandler
. In this case, separately write decorators for ICommandHandler
and IQueryHandler
, for example, for independent transaction management.Decorators for Application Service
We can also use the IUseCaseHandler
interface in Application Services if we use an explicit implementation .
public class AppService : IAppService : IUseCaseHandler<RequestType1 , ResponseType1> : IUseCaseHandler<RequestType2 , ResponseType2> : IUseCaseHandler<RequestType3, ResponseType3> //... : IUseCaseHandler<RequestTypeN, RequestTypeN> { public ResponseType1 UseCase1(RequestType1 request) { //... } IUseCaseHandler<RequestType1 , ResponseType1>.Handle(RequestType1 request) => UseCase1(request); //... ResponseTypeN UseCaseN(RequestTypeN request) { //... } IUseCaseHandler<RequestTypeN , ResponseTypeN>.Handle(RequestTypeN request) => UseCaseN(request); //... }
In the application code, you must use the IUseCaseHandler
interfaces, not the IAppService
, because 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 question .
void ICommandHandler<TCommand>.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 specify in the method signature that execution may fail, the example above can be rewritten as:
Result ICommandHandler<TCommand>.Handle(TCommand command) { return this.validator.ValidateObject(command) && this.decoratee.Handle(command); }
Thus, it will be possible to further divide the decorators by the type of the return value. For example, log methods that return a Result
different way than methods that return unopened values.
All Articles