No More Data Transfer Objects: A Guide to Adopting CQRS ππ
In the software engineering industry, the term CQRS is used quite often. It describes a pattern which allows you to handle services in your system in a vertical manner. It gives you the ability to free yourself from any other service in the same domain and allows you to focus on the service that you wish to implement.
I know that there are hundreds of other resources that can explain the CQRS pattern better in terms of definition and philosophy. However, in this article I'm going to explain it in another way, a rather practical way.
Imagine you have an entity named Customer. Normally if we follow clean architecture, we will have a single horizontally scaled service called CustomerService. In this CustomerService there are all types of services that perform different operations on this entity. However not all services need to operate on the whole entity model.
This is why Data Transfer Objects exist. Data Transfer Objects (DTOs) are a simplified versions of entity models which allow you to focus only on the fields you require from this entity or even add more fields which will help the service but do not necessarily belong to the entity itself.
For example, I want to add a new customer but updating the customer requires less fields than creating one. So, if I create only one DTO for both services I will have empty unnecessary fields in case of update. So, you will probably find yourself creating a DTO for creating a customer and another one for updating a customer.
Okay, but now you need to retrieve loyal customers only who have made more than 10 orders. Normally you would send the number of orders as a parameter to the controller and then the controller sends the number of orders to the service. But what if now you need to change loyal customers to only a certain vendor. You would have to add another parameter to your controller to and your service.
In such cases, CQRS comes in handy because in our first example you only need to specify a CreateCustomerCommand class and you put the needed fields in it, and then add a handler to it which creates a new customer. Similarly, create a UpdateCustomerCommand class with the required fields to update a customer then add its handler which updates the customer.
Same with fetching loyal customers. You just create a GetLoyalCustomers class where you add the needed fields to get the loyal customers and a handler for this query which fetches the customers with the query specified. Then, the controller doesn't have to change its parameters each time you update your query requirements because it will always be expecting a GetLoyalCustomers query.
Let's apply this in a simple example so that it would be clearer. In this article I will be showing you how to Create a:
- Command that doesn't return a result
- Command that returns a result
- Query
- Message dispatcher that is responsible for matching the command/query with its handler
Interfaces π
public interface IMessage{// Marker interface for messages that don't return a result}public interface IMessage<TResult>{// Marker interface for messages that return a result}
public interface ICommand : IMessage{// Marker interface for commands that don't return a result}public interface ICommand<TResult> : IMessage<TResult>{// Marker interface for commands that return a result}
public interface IQuery<TResult> : IMessage<TResult>{// Marker interface for queries}
public interface ICommandHandler<TCommand> where TCommand : ICommand{void Handle(TCommand command);}public interface ICommandHandler<TCommand, TResult> where TCommand : ICommand<TResult>{TResult Handle(TCommand command);}
public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>{TResult Handle(TQuery query);}
Command π
public class AddTwoNumbersCommand(int number1, int number2) : ICommand<int>{public int Number1 { get; set; } = number1;public int Number2 { get; set; } = number2;}public class AddTwoNumbersCommandHandler : ICommandHandler<AddTwoNumbersCommand, int>{public int Handle(AddTwoNumbersCommand command) => command.Number1 + command.Number2;}
public class LogMessageCommand(string message) : ICommand{public string Message { get; set; } = message;}public class LogMessageCommandHandler : ICommandHandler<LogMessageCommand>{public void Handle(LogMessageCommand command) => Console.WriteLine(command.Message);}
builder.Services.AddTransient<ICommandHandler<AddTwoNumbersCommand, int>, AddTwoNumbersCommandHandler>();builder.Services.AddTransient<ICommandHandler<LogMessageCommand>, LogMessageCommandHandler>();
Query π
public class Customer(){public string Name { get; set; } = string.Empty;public int Orders { get; set; }}public class GetLoyalCustomersQuery(int minimumOrders) : IQuery<IEnumerable<Customer>>{public int MinimumOrders { get; } = minimumOrders;}public class GetLoyalCustomerQueryHandler : IQueryHandler<GetLoyalCustomersQuery, IEnumerable<Customer>>{private readonly List<Customer> customers = [new Customer { Name = "Omar", Orders = 10 }, new Customer { Name = "Ahmed", Orders = 20 }, new Customer { Name = "Mosad", Orders = 30 }];public IEnumerable<Customer> Handle(GetLoyalCustomersQuery query) => customers.Where(c => c.Orders > query.MinimumOrders);}
Let's go ahead and register this query in program.cs.
builder.Services.AddTransient<IQueryHandler<GetLoyalCustomersQuery, IEnumerable<Customer>>, GetLoyalCustomersQueryHandler>();But wait! What's going to piece this together? How will our application know that this handler belongs to this query or command? The answer is the message dispatcher.
Message Dispatcher π©
public class MessageDispatcher(IServiceProvider serviceProvider){// Dispatch for queriespublic TResult? Dispatch<TResult>(IQuery<TResult> query) => Handle(query, typeof(IQueryHandler<,>));// Dispatch for commands that return a resultpublic TResult? Dispatch<TResult>(ICommand<TResult> command) => Handle(command, typeof(ICommandHandler<,>));// Dispatch for commands that don't return a resultpublic void Dispatch(ICommand command){var commandType = command.GetType();var handlerType = typeof(ICommandHandler<>).MakeGenericType(commandType);// The dynamic type is used to call the Handle method of the handlerdynamic? handler = serviceProvider.GetService(handlerType);// The dynamic handler will be resolved at runtimehandler?.Handle((dynamic)command);}// A generic method to handle all types of messages (commands or queries) that return a resultprivate TResult? Handle<TResult>(IMessage<TResult> message, Type handlerType){var commandType = message.GetType();var GenericHandlerType = handlerType.MakeGenericType(commandType, typeof(TResult));// The dynamic type is used to call the Handle method of the handlerdynamic? handler = serviceProvider.GetService(GenericHandlerType);if (handler != null){// The dynamic handler will be resolved at runtimereturn handler.Handle((dynamic)message);}return default;}}
- Dispatch for queries
- Dispatch for commands with results
- Dispatch for commands without results
- A generic handle function to get the handler type and handles
builder.Services.AddSingleton<MessageDispatcher>();
Controller π
[ApiController][Route("[controller]")]public class CQRSController(MessageDispatcher messageDispatcher) : ControllerBase{[HttpPost][Route("get-loyal-customers")]public IEnumerable<Customer> GetLoyalCustomers(GetLoyalCustomersQuery getLoyalCustomersQuery){return messageDispatcher.Dispatch(getLoyalCustomersQuery) ?? [];}[HttpPost][Route("add-two-numbers")]public int AddTwoNumbers(AddTwoNumbersCommand addTwoNumbersCommand){return messageDispatcher.Dispatch(addTwoNumbersCommand);}[HttpPost][Route("log-message")]public void LogMessage(LogMessageCommand logMessageCommand){messageDispatcher.Dispatch(logMessageCommand);}}
Comments
Post a Comment