ICommand
to ICommandHandler
public class PayOrderCommand { public int OrderId { get; set; } public void Execute() { //... } }
public class PayOrderCommand { public int OrderId { get; set; } public PayOrderCommand(IUnitOfWork unitOfWork) { // WAT? } public void Execute() { //... } }
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.
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:CreateOrderCommandHandler
and then IdentityQueryHandler<Order>
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).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 .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 betweenvoid
andvoid
even less noticeable in F #. The valuevoid
in F # corresponds to the typeUnit
.Unit
in functional programming languages ​​is a kind of singleton without values. Thus, the difference betweenvoid
andvoid
is due to technical implementation, not abstraction. Read more aboutvoid
andunit
in Mark Siman’s blog.
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.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 .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. public interface IQuery<TResult> { } public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult> { TResult Handle(TQuery query); }
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); }
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; } }
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.
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.QueryHandler
entities or DTO?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.
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.Castle.Dynamic Proxy
or rewriting IL at compile time, for example PostSharp
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); } }
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)); }
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); }
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