📜 ⬆️ ⬇️

CQRS. Facts and Fallacies


CQRS is an architecture style in which read operations are separated from write operations. The approach was formulated by Greg Young based on the CQS principle proposed by Bertrand Meyer. Most often (but not always) CQRS is implemented in restricted contexts ( bounded context ) of applications designed on the basis of DDD. One of the natural reasons for the development of CQRS is the non-symmetrical distribution of the load and complexity of business logic on the read and write subsystems. Most business rules and complex checks are in the write subsystem. At the same time, data is often read many times more often than they change.

Despite the simplicity of the concept, the details of the implementation of the CQRS may differ significantly. And this is exactly the case when the devil is in the details.

From ICommand to ICommandHandler


Many start implementing CQRS with the use of the “ team ” pattern, combining data and behavior in the same class.

 public class PayOrderCommand { public int OrderId { get; set; } public void Execute() { //... } } 

This complicates the serialization / deserialization of commands and the introduction of dependencies.
')
 public class PayOrderCommand { public int OrderId { get; set; } public PayOrderCommand(IUnitOfWork unitOfWork) { // WAT? } public void Execute() { //... } } 

Therefore, the original command is divided into “data” - DTO and behavior “command handler”. Thus, the “command” itself no longer contains dependencies and can be used as a Parameter Object , incl. as an argument to the controller.

 public interface ICommandHandler<T> { public void Handle(T command) { //... } } public class PayOrderCommand { public int OrderId { get; set; } } public class PayOrderCommandHandler: ICommandHandler<PayOrderCommand> { public void Handle(PayOrderCommand command) { //... } } 

If you want to use entities, rather than their Id in commands, so as not to validate inside handlers, you can override the Model Binding , although this approach has shortcomings . A little later, we will look at how to validate without changing the standard Model Binidng.

Should ICommandHandler always return void?


Handlers do not read, for this there is a read subsystem and part of the Query, so they should always return void . But what about the Id generated by the database? For example, we sent the command “place an order”. Order number corresponds to its Id from the database. Id cannot be obtained until the INSERT request is completed. What people won’t think up, what to bypass this made-up restriction:

  1. Call CreateOrderCommandHandler and then IdentityQueryHandler<Order>
  2. Out - parameters
  3. Adding special properties to the command for the return value
  4. Developments
  5. Waiver of auto-increment Id in favor of Guid. Guid come in the body of the command and recorded in the database

Well, what about validation that cannot be done without a query to the database, for example, the presence in the database of an entity with a given Id or a client’s account status? Everything is simple here. Most often, they simply throw an exception, despite the fact that there is nothing “exceptional” in validation.

Greg Young clearly states his position on this issue (25 minutes): “ Should the command handler always return void ? No, the list of errors or an exception may be the result of the execution . ” The handler can return the result of the operation. It should not be engaged in the work of Query - data retrieval, which does not mean that it cannot return a value. The main limitation on this is your system requirements and the need to use the asynchronous interaction model. If you know for sure that the command will not be executed synchronously, but instead will be placed in a queue and processed later, do not expect to receive an Id in the context of an HTTP request. You can get a Guid operation and poll the status, provide a callback or get an answer on the web sockets. In any case, void or non void in the handler is the least of your problems. An asynchronous model will make the entire user experience change, including the interface (see how the search for tickets to Ozon or Aviasales looks like).

You should not expect that void as a return value will allow the use of a single code base for synchronous and asynchronous models. The absence of a meaningful return result can be misleading for consumers of your API. By the way, using exceptions for the control flow, you still return the value from the handler, just do it implicitly, violating the principle of structured programming .

Just in case, on one of the DotNext, I asked Dino Esposito's opinion on this. He agrees with Young: the handler can return a response. This may not be void , but it must be the result of the operation, not the data from the database. CQRS is a high-level concept, giving a win in some situations (different requirements for the read and write subsystems), and not a dogma.
The distinction between void and void even less noticeable in F #. The value void in F # corresponds to the type Unit . Unit in functional programming languages ​​is a kind of singleton without values. Thus, the difference between void and void is due to technical implementation, not abstraction. Read more about void and unit in Mark Siman’s blog.

And what about Query?


Query in CQRS can somehow remind Query Object . However, in reality these are different abstractions. Query Object - a specialized pattern for generating SQL using an object model. In .NET, with the advent of LINQ and Expression Trees pattern has lost its relevance. Query in CQRS is a request for receiving data in a convenient form for the client.

By analogy with the Command CommandHandler it is logical to separate Query and QueryHandler . And in this case, QueryHandler really cannot return void anymore. If nothing was found on the request, we can return null or use the Special Case .

But what is the fundamental difference between the CommandHandler<TIn, TOut> and QueryHandler<TIn, TOut> ? Their signatures are the same. The answer is the same. The difference in semantics. QueryHandler returns data and does not change the state of the system. CommandHandler , on the contrary, changes its state and, possibly , returns the status of the operation.

If one semantics is not enough for you, you can make such changes to the interface:

 public interface IQuery<TResult> { } public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult> { TResult Handle(TQuery query); } 

The type TResult additionally emphasizes that the query has a return value and even binds it to it. I spotted this implementation in the blog of the developer Simple Injector and co-author of the book Dependency Injection in .NET Stephen van Deyrsen. In our implementation, we limited ourselves to changing the name of the method from Handle to Ask , so that we can immediately see on the IDE screen that the request is being executed without having to specify the type of the object.

 public interface IQueryHandler<TQuery, TResult> { TResult Ask(TQuery query); } 

Do we need other interfaces?


At some point it may seem that all other data access interfaces can be put into junk. We take several QueryHandler' , collect handler for more, more of them, and so on. QueryHandler' makes sense only if you have separate use cases A and B and you need another use case, which returns A + B data without additional conversions. By the type of the return value, it is not always obvious that it will return a QueryHandler . Therefore, it is easy to get confused in interfaces with different generic parameters. In addition, C # is verbose.

 public class SomeComplexQueryHandler { IQueryHandler<FindUsersQuery, IQueryable<UserInfo>> findUsers; IQueryHandler<GetUsersByRolesQuery, IEnumerable<User>> getUsers; IQueryHandler<GetHighUsageUsersQuery, IEnumerable<UserInfo>> getHighUsage; public SomeComplexQueryHandler( IQueryHandler<FindUsersQuery, IQueryable<UserInfo>> findUsers, IQueryHandler<GetUsersByRolesQuery, IEnumerable<User>> getUsers, IQueryHandler<GetHighUsageUsersQuery, IEnumerable<UserInfo>> getHighUsage) { this.findUsers = findUsers; this.getUsers = getUsers; this.getHighUsage = getHighUsage; } } 

It is more convenient to use QueryHandler as an entry point for a specific use case. And to get data inside create specialized interfaces. So the code will be more readable.
If the idea of ​​arranging small functions into large ones does not give you peace of mind, then consider the option of changing the programming language. In F #, this idea is embodied much better.

Can the write subsystem use the read subsystem and vice versa?


Another dogma is that you should never mix write and read subsystems. Strictly speaking, everything is correct here. If you wanted to use get data from QueryHandler inside the command handler, it most likely means that you do not need CQRS in this subsystem. CQRS solves a specific problem: read - the subsystem does not cope with loads.

Until recently, one of the most popular questions in the DDD group was: “We use DDD and we have an annual report here. When we try to build it, our business logic layer pulls aggregates into RAM and the RAM ends. How should we be? Clear as: write an optimized SQL query manually. The same applies to the visited web resources. There is no need to raise all the OOP-splendor to get the data, cache and display. CQRS - offers an excellent watershed: we use domain logic in command handlers, because there are not so many teams and because we want all business rule checks to be executed. In the read subsystem, on the contrary, it is desirable to bypass the business logic layer, because it slows down.

By mixing the read and write subsystems, we lose the watershed. The meaning of semantic abstraction is lost even at the level of a single repository. In the case when the read subsystem uses another data storage, there is generally no guarantee that the system is in a consistent state. Once the relevance of the data is not guaranteed, the meaning of the business layer checks is lost. Using the write subsystem in the read subsystem is generally contrary to the meaning of the operation: commands, by definition, change the state of the system, but the query does not.

Each rule, however, has exceptions. In the same video a minute before, Greg gives an example: “you need to load millions of entities to do the calculation. Will you load all this data into RAM or perform an optimal query? ”. If the read subsystem already has a suitable query handler and you are using one data source, no one will put you in jail for calling query from the command handler. Just keep in your mind the arguments against it.

To return from QueryHandler entities or DTO?


DTO. If the customer requires the entire unit from the database, something is wrong with the customer. Moreover, as flat data as possible is usually required. You can start using LINQ and Queryable Extensions or Mapster during the prototyping phase. And if necessary, replace the implementation of QueryHandler with Dapper and / or other data storage. In Simple Injector there is a convenient mechanism : you can register all objects that implement the interfaces of open generics from the assembly, and for the rest, leave a fallback with LINQ. Writing such a configuration once will not have to edit it. It is enough to add a new implementation to the assembly and the container automatically picks up. For other generics, the folback will continue to work on the LINQ implementation. Mapster , by the way, does not require creating profiles for mapping. If you comply with the agreement in the names of the properties between Entity and Dto projection will be built automatically.
We have the following rule with the “auto-mapper”: if you need to write manual mapping and the built-in agreements are not enough, it is better to do without the auto-pager. Thus, the move to the mapstar was quite simple.

CommandHandler and QueryHandler - holistic abstractions


Those. valid from the beginning to the end of the transaction. Those. Typical use is one handler per request. To access data, it is better to use other mechanisms, for example, the already mentioned QueryObject or UnitOfWork . By the way, this solves the problem using Query from Command and vice versa. Just use QueryObject both there and there. Violation of this rule complicates the management of transactions and connection to the database.

Cross Cutting Concerns and Decorators


CQRS has one big advantage over the standard service architecture: we have only 2 generic interfaces. This allows you to multiply the usefulness of the template " decorator ". There are a number of functions that any application needs but are not business logic in the direct sense: logging, error handling, transactionalism, etc. Traditionally, two options:

  1. accept and trash business logic with such dependencies and related code
  2. look towards AOP: using spoilers at runtime, for example Castle.Dynamic Proxy or rewriting IL at compile time, for example PostSharp

The first option is bad for its verbosity and copy-paste. The second - problems with performance and debugging, dependence on external tools and "magic". Option with decorators - is somewhere in the middle. On the one hand, we can take the accompanying logic to the decorators. On the other hand, there is no magic in it. All code is written by man and can be debugged.

Remember, I promised to solve the problem by validating the input parameters without changing the ModelBinder? Here is the answer, implement the decorator for validation. If you are satisfied with the use of exceptions, then throw out the ValidationExcepton .

 public class ValidationQueryHandlerDecorator<TQuery, TResult> : IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult> { private readonly IQueryHandler<TQuery, TResult> decorated; public ValidationQueryHandlerDecorator(IQueryHandler<TQuery, TResult> decorated) { this.decorated = decorated; } public TResult Handle(TQuery query) { var validationContext = new ValidationContext(query, null, null); Validator.ValidateObject(query, validationContext, validateAllProperties: true); return this.decorated.Handle(query); } } 

If not, you can make a small wrapper and use Result as the return value.

  public class ResultQueryHandler<TSource, TDestination> : IQueryHandler<TSource, Result<TDestination>> { private readonly IQueryHandler<TSource, TDestination> _queryHandler; public ResultQueryHandler(IQueryHandler<TSource, TDestination> queryHandler) { _queryHandler = queryHandler; } public Result<TDestination> Ask(TSource param) => Result.Succeed(_queryHandler.Ask(param)); } 

SimpleInjector offers a convenient way to register open generics and decorators . With just one line of code, you can insert logging before executing, after executing, hang global transactionality, error handling, automatic subscription to domain events. The main thing is not to overdo it.

There is a certain inconvenience with two interfaces IQueryHandler and ICommandHandler . If we want to enable logging or validation in both subsystems, we will have to write two decorators, with the same code. Well, this is not a typical situation. In the read subsystem, transactionism is hardly required. Nevertheless, examples with validation and logging are quite vital. You can solve this problem by moving from interfaces to delegates.

 public abstract class ResultCommandQueryHandlerDecorator<TSource, TDestination> : IQueryHandler<TSource, Result<TDestination>> , ICommandHandler<TSource, Result<TDestination>> { private readonly Func<TSource, Result<TDestination>> _func; //      protected ResultCommandQueryCommandHandlerDecorator( Func<TSource, Result<TDestination>> func) { _func = func; } //  Query protected ResultCommandQueryCommandHandlerDecorator( IQueryHandler<TSource, Result<TDestination>> query) : this(query.Ask) { } //  Command protected ResultCommandQueryCommandHandlerDecorator( ICommandHandler<TSource, Result<TDestination>> query) : this(query.Handle) { } protected abstract Result<TDestination> Decorate( Func<TSource, Result<TDestination>> func, TSource value); public Result<TDestination> Ask(TSource param) => Decorate(_func, param); public Result<TDestination> Handle(TSource command) => Decorate(_func, command); } 

Yes, in this case there is also a small overhead: you have to declare two classes only for casting the parameter passed to the constructor. This can also be solved by complicating the configuration of the IOC container, but it is easier for me to declare two classes.

An alternative is to use the IRequestHandler interface for Command and Query , and not to be confused to use naming convention. This approach is implemented in the MediatR library.

Source: https://habr.com/ru/post/347908/


All Articles