Skip to content

govorovvs/Quarks.CQRS

Repository files navigation

Quarks.CQRS

Version

Command and Query Responsibility Segregation (CQRS) Pattern

Command and Query Responsibility Segregation (CQRS) is a pattern that segregates the operations that read data (Queries) from the operations that update data (Commands) by using separate interfaces.

cqrs schema

Query example

public class GetUserByIdQuery(int id) : IQuery<UserModel>
{
    public int Id { get; } = id;
}

public class UserController : Controller
{
    private readonly IQueryDispatcher _queryDispatcher;

    [HttpGet, Route("{id:int}")]
    public async Task<UserModel> GetAsync(int id, CancellationToken cancellationToken)
    {
        GetUserByIdQuery query = new GetUserByIdQuery(id);
        UserModel model = await _queryDispatcher.DispatchAsync(query, cancellationToken);
        return model;
    }
}

public class GetUserByIdQueryHandler : IQueryHandler<GetUserByIdQuery, UserModel>
{
    private readonly IUserRepository _userRepository;
    private readonly IUserModelMapper _userModelMapper;

    public async Task<UserModel> HandleAsync(GetUserByIdQuery query, CancellationToken cancellationToken)
    {
        User user = await _userRepository.FindByIdAsync(query.Id, cancellationToken);
        if (user == null)
            return null;

        UserModel model = _userModelMapper.MapToModel(user);
        return model;
    }
}

Command example

public class RenameUserCommand(int id, string name) : ICommand
{
    public int Id { get; } = id;
    public string Name { get; } = name;
}

public class UserController : Controller
{
    private readonly ICommandDispatcher _commandDispatcher;

    [HttpPost, Route("{id:int}")]
    public async Task<IHttpResult> RenameAsync(int id, string name, CancellationToken cancellationToken)
    {
        RenameUserCommand command = new RenameUserCommand(id, name);
        await _commandDispatcher.DispatchAsync(command, cancellationToken);
        return Ok();
    }
}

public class RenameUserCommandHandler : ICommandHandler<RenameUserCommand>
{
    private readonly IUserRepository _userRepository;

    public async Task HandleAsync(RenameUserCommand command, CancellationToken cancellationToken)
    {
        using (ITransaction transaction = Transaction.Begin())
        {
            User user = _userRepository.FindByIdAsync(command.Id, cancellationToken);
            if (user == null)
                throw new EntityNotFoundException("User", command.Id);

            user.Rename(command.Name);

            await _userRepository.ModifyAsync(user, cancellationToken);
            await transaction.CommitAsync(cancellationToken);
        }
    }
}

Default command/query dispatchers

Library has default implementations of ICommandDispatcher and IQueryDispatcher based on IServiceProvider.

public class CommandDispatcher : ICommandDispatcher
{
    private readonly IServiceProvider _serviceProvider;

    public Task DispatchAsync<TCommand>(TCommand command, CancellationToken cancellationToken) where TCommand : ICommand
    {
        ICommandHandler<TCommand> handler = _serviceProvider.GetService(typeof (ICommandHandler<TCommand>));
        return handler.HandleAsync(command, cancellationToken);
    }
}

public class QueryDispatcher : IQueryDispatcher
{
    private readonly IServiceProvider _serviceProvider;

    public Task<TResult> DispatchAsync<TResult>(IQuery<TResult> query, CancellationToken cancellationToken)
    {
        Type concreteHandlerType = typeof(IQueryHandler<,>).MakeGenericType(query.GetType(), typeof(TResult));
        object handler =  _serviceProvider.GetService(concreteHandlerType);
        MethodInfo method = handler.GetType().GetRuntimeMethod("HandleAsync", new[] {query.GetType(), cancellationToken.GetType()});
        return (Task<TResult>) method.Invoke(handler, new object[] {query, cancellationToken});
    }
}

Handler factories via IoC

The simplest way to impplement handler factory is to use IoC container. Here is an example uses ServiceCollection

public static class CqrsServiceCollectionExtensions
{
    public void RegisterCQRS(this IServiceCollection services)
    {
        services.TryAddTransient<ICommandDispatcher, CommandDispatcher>();
        services.TryAddTransient<IQueryDispatcher, QueryDispatcher>();
        return services;
    );
}