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

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,
ICommandHandler
validation 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
ICommandHandler
and 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 IHandler
andApplication 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
IRequestHandler
and using it for Command
and 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 .IRequestHandler
→ IUseCaseHandler
Why not rename the interface
IRequestHandler
to 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 ICommandHandler
and IQueryHandler
, for example, for independent transaction management.Decorators for Application Service
IUseCaseHandler
We 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 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 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
Result
differently than methods that return untracked values.