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:

  1. Command that doesn't return a result
  2. Command that returns a result
  3. Query
  4. Message dispatcher that is responsible for matching the command/query with its handler
Note that I have already talked about how to implement the CQRS pattern using a library such as MediatR in a previous post but in this post, we'll be focusing on how to implement it ourselves from scratch.

Interfaces πŸ“‹

Before we start anything, we need to define our interfaces first so that we would have a clear vision of our implementation. The first interface we need to define is the IMessage which is basically the interface that the command and query should implement.
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
}
As you can see, I have declared two interfaces one for the command that we don't expect a result from and another for the commands that return a result and the queries. Now let's go ahead and define the commands and queries.
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
}
These should be in an ICommand interface, and I have added a generic parameter TResult to represent the generic response of the command. The TResult will be defined when the create the command.
public interface IQuery<TResult> : IMessage<TResult>
{
    // Marker interface for queries
}
This should exist in an IQuery interface. I have also added a generic parameter TResult.

Now we need to define interfaces for the handlers. We need those so that we can match our handlers with our messages when dispatching the messages.

First, we specify the command handlers which are two, one the returns a result and the other does not.
public interface ICommandHandler<TCommand> where TCommand : ICommand
{
    void Handle(TCommand command);
}
public interface ICommandHandler<TCommand, TResult> where TCommand : ICommand<TResult>
{
    TResult Handle(TCommand command);
}
Here I'm specifing a generic parameter TCommand which implements the ICommand interface we defined above.

Now let's define the query handler interface.
public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
    TResult Handle(TQuery query);
}
Same here with TQuery. This is the power of generic parameters. They allow your code to be more generic and reusable.

Make sure you placed these interfaces in a folder named Contracts or Interfaces.

Great! Now that we've defined our interfaces let's implement some commands and queries.

Command πŸ“

We'll create two simple commands to try out both types of commands. The first one will perform an addition operation and return the result. The other will just log a message on the application console.

We'll start with the addition command and its handler. Note that the commands should be placed in a folder named Commands and the handlers should be placed in a folder named Handlers or CommandHandlers. Some people might place the handler inside the command itself but it's a matter of order you can organize your project any way you want.
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;
}
Notice how we added the TResult as int so the TResult is not an unknown anymore. Also, in the handler we specified the command we want to handle so we also satisfied the TCommand generic parameter.

Perfect now let's create a command that doesn't return a result.
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);
}
We've also defined the TCommand as the LogMessageCommand so we've satisfied the generic parameter of ICommandHandler.

Let's register these two commands in programs.cs.
builder.Services.AddTransient<ICommandHandler<AddTwoNumbersCommand, int>, AddTwoNumbersCommandHandler>();
builder.Services.AddTransient<ICommandHandler<LogMessageCommand>, LogMessageCommandHandler>();
Perfect! Now let's head on to the next section where we define a simple query.

Query πŸ”Ž

When defining a query, you need to add all the fields that would help you retrieve the desired results. So, I will create a query that retrieves loyal customers. A loyal customer is a customer that made more than 10 orders.

So, first I will define a Customer class and I will mock the database as a list of customers with different number of orders. And my handler will filter this list by the number of orders that exist in the 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);
}
Just like the commands we defined above we've specified the TResult and the TQuery and now we're ready to try these commands and queries.

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 πŸ“©

If you are familiar with the mediator pattern, you will understand that this is the role of the message dispatcher. We need a service that, given a message it will seek its handler, invoke its Handle function and finally return the expected result.
public class MessageDispatcher(IServiceProvider serviceProvider)
{
    // Dispatch for queries
    public TResult? Dispatch<TResult>(IQuery<TResult> query) => Handle(query, typeof(IQueryHandler<,>));

    // Dispatch for commands that return a result
    public TResult? Dispatch<TResult>(ICommand<TResult> command) => Handle(command, typeof(ICommandHandler<,>));

    // Dispatch for commands that don't return a result
    public 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 handler
        dynamic? handler = serviceProvider.GetService(handlerType);

        // The dynamic handler will be resolved at runtime
        handler?.Handle((dynamic)command);
    }

    // A generic method to handle all types of messages (commands or queries) that return a result
    private 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 handler
        dynamic? handler = serviceProvider.GetService(GenericHandlerType);

        if (handler != null)
        {
            // The dynamic handler will be resolved at runtime
            return handler.Handle((dynamic)message);
        }

        return default;
    }
}
In this message dispatcher service, I have created 4 dispatch functions:
  1. Dispatch for queries
  2. Dispatch for commands with results
  3. Dispatch for commands without results
  4. A generic handle function to get the handler type and handles
Notice how I used dynamic to handle the unknown type of handler. If the handler does not have a Handle function to invoke the code will cause a runtime error. But this should be impossible as we have specified it in our interface.

Let's finally register this dispatcher as a singleton in our program.cs.

builder.Services.AddSingleton<MessageDispatcher>();

Perfect! Now that we have our mediator ready, let's set up a controller to allow us to try our work so far.

Controller πŸ”—

I will set up a simple controller that calls the injected dispatcher to resolve our messages.
[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);
    }
}
Just 3 APIs that dispatch the different messages we defined. Now let's go ahead and run our application to check the output.

CQRS in Motion ⚙️

Let's first try our logging command that does not return a result.


Excellent! Our message was logged. This means that our dispatcher found the handler and invoked the handle function to log the message. Let's try the addition command that is supposed to return the result.


This command also seems to be working fine. Finally let's check our query. If you look at my static list that I defined inside the handler to mock the customer repository, you will see that there are three records but only two of them have orders that exceed 10 orders. Let's give the query a minimum number of orders of 11 and see if it returns only two customers.


Our query is working fine as well. This concludes our tutorial and I hope you have learned how this sort of mechanism work behind the scenes before using a library that automatically sets everything up like MediatR or other CQRS libraries. 

This setup will allow you to add as many command or queries as you want and all you have to do is make them implement the interfaces and it should work fine. But actually, that's not true. You have to register each service you want to add in the program.cs in a tedious manner. In later posts I will show you how to make a generic service that scans the assembly for all commands and queries and registers them to your application.

Comments

Popular posts

Google & Microsoft Token Validation Middleware in ASP.NET Applications

Publish and Consume Messages Using RabbitMQ as a Message Broker in 4 Simple Steps 🐰

Real Life Case Study: Synchronous Inter-Service Communication in Distributed Systems