A set of pure CQRS implementation by hand

A set of pure CQRS implementation by hand

Regarding CQRS, there are many differences in implementation. This is because CQRS itself is very simple, but it is like the key to Pandora's Box. With it, read-write separation, event traceability, message transfer, and eventual consistency have all been introduced into the framework. , Which led to too much confusion for CQRS. This article aims to provide a simple implementation of CQRS, which does not depend on concepts such as ES and Messaging, but only focuses on CQRS itself.

What is the essence of CQRS? My understanding is that it separates reading and writing, uses different data models for reading and writing, and creates corresponding reading and writing objects according to responsibilities; all other concepts are extensions of CQRS.

The following pseudo code will show the essence of CQRS:

Before using CQRS:

CustomerService

void MakeCustomerPreferred(CustomerId) 
Customer GetCustomer(CustomerId) 
CustomerSet GetCustomersWithName(Name) 
CustomerSet GetPreferredCustomers() 
void ChangeCustomerLocale(CustomerId, NewLocale) 
void CreateCustomer(Customer) 
void EditCustomerDetails(CustomerDetails)

After using CQRS:

CustomerWriteService

void MakeCustomerPreferred(CustomerId) 
void ChangeCustomerLocale(CustomerId, NewLocale) 
void CreateCustomer(Customer) 
void EditCustomerDetails(CustomerDetails)

CustomerReadService

Customer GetCustomer(CustomerId) 
CustomerSet GetCustomersWithName(Name) 
CustomerSet GetPreferredCustomers()

Query

Query: returns the result, but does not change the state of the object, and has no side effects on the system.

The implementation of the query is relatively simple, we first define a read-only warehouse:

public interface IReadonlyBookRepository
{
    IList<BookItemDto> GetBooks();

    BookDto GetById(string id);
}

Then use it in the Controller:

public IActionResult Index()
{
    var books = readonlyBookRepository.GetBooks();

    return View(books);
}

Command

Command: Does not return any result (void), but changes the state of the object.

The command represents the user's intention and contains business data.

First define the ICommand interface, which does not contain any methods and properties, and is only used as a mark.

public interface ICommand
{
    
}

There is a CommandHandler corresponding to the Command, and specific operations are defined in the Handler.

public interface ICommandHandler<TCommand>
    where TCommand: ICommand
{
    void Execute(TCommand command);
}

In order to be able to encapsulate the positioning of the Handler, we also need to define an ICommandHandlerFactory:

public interface ICommandHandlerFactory
{
    ICommandHandler<T> GetHandler<T>() where T: ICommand;
}

Implementation of ICommandHandlerFactory:

public class CommandHandlerFactory: ICommandHandlerFactory
{
    private readonly IServiceProvider serviceProvider;

    public CommandHandlerFactory(IServiceProvider serviceProvider) 
    {
        this.serviceProvider = serviceProvider;
    }

    public ICommandHandler<T> GetHandler<T>() where T: ICommand
    {
        var types = GetHandlerTypes<T>();
        if (!types.Any())
        {
            return null;
        }
        
       //Instantiate Handler
        var handler = this.serviceProvider.GetService(types.FirstOrDefault()) as ICommandHandler<T>;
        return handler;
    }

   //This code comes from the Diary.CQRS project and is used to find the CommandHandler corresponding to the Command
    private IEnumerable<Type> GetHandlerTypes<T>() where T: ICommand
    {
        var handlers = typeof(ICommandHandler<>).Assembly.GetExportedTypes()
            .Where(x => x.GetInterfaces()
                .Any(a => a.IsGenericType && a.GetGenericTypeDefinition() == typeof(ICommandHandler<>)))
                .Where(h => h.GetInterfaces()
                    .Any(ii => ii.GetGenericArguments()
                        .Any(aa => aa == typeof(T)))).ToList();


        return handlers;
    }

Then we define an ICommandBus, ICommandBus sends and executes commands through the Send method. It is defined as follows:

public interface ICommandBus
{
    void Send<T>(T command) where T: ICommand;
}

Implementation of ICommandBus:

public class CommandBus: ICommandBus
{
    private readonly ICommandHandlerFactory handlerFactory;

    public CommandBus(ICommandHandlerFactory handlerFactory)
    {
        this.handlerFactory = handlerFactory;
    }

    public void Send<T>(T command) where T: ICommand
    {
        var handler = handlerFactory.GetHandler<T>();
        if (handler == null)
        {
            throw new Exception("The corresponding handler was not found");
        }

        handler.Execute(command);
    }
}

Let's define a new command CreateBookCommand:

public class CreateBookCommand: ICommand
{
    public CreateBookCommand(CreateBookDto dto)
    {
        this.Dto = dto;
    }

    public CreateBookDto Dto {get; set;}
}

I don’t know if it is reasonable to use DTO object to initialize directly.

The Handler corresponding to CreateBookCommand is as follows:

public class CreateBookCommandHandler: ICommandHandler<CreateBookCommand>
{
    private readonly IWritableBookRepository bookWritableRepository;

    public CreateBookCommandHandler(IWritableBookRepository bookWritableRepository)
    {
        this.bookWritableRepository = bookWritableRepository;
    }

    public void Execute(CreateBookCommand command)
    {
        bookWritableRepository.CreateBook(command.Dto);
    }
}

When we use it in the Controller, the code looks like this:

[HttpPost]
public IActionResult Create(CreateBookDto dto)
{
    dto.Id = Guid.NewGuid().ToString("N");
    var command = new CreateBookCommand(dto);
    commandBus.Send(command);

    return Redirect("~/book");
}

The UI layer does not need to understand the execution process of the Command, it only needs to send the command through the CommandBus, and the front-end operation is also very simple.

The complete code of this example is on github, if you are interested, please move >> https://github.com/qifei2012/sample_cqrs

If there is an error or inappropriate place in the code, please point it out in the comments, thank you for your support.

Reference documents

Reference: https://cloud.tencent.com/developer/article/1511941 hand-drawn a set of pure CQRS implementation-Cloud + Community-Tencent Cloud