Skip to content

A library to work easier with CQRS on top of MediatR.

Notifications You must be signed in to change notification settings

tuliopaim/EasyCqrs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

EasyCqrs

A library to work easier with CQRS on top of MediatR.

Table of Contents

Install

CLI

dotnet add package TP.EasyCqrs

Package Manager Console

Install-Package TP.EasyCqrs

Features

  • Auto injected Handlers
  • Pipelines
    • Validation Pipeline - Auto validate inputs before entering the handler
  • Result / Result<T>
  • Error

Read more about Cqrs MediatR MediatR Pipeline Behavior


Basic Concepts

The main idea is that you can create an application and setup to work with CQRS very easly.

You can structure the application in such a way that all classes related to that specific command or query or event are in the same directory, for example:

  • Commands
    • NewPersonCommand
      • NewPersonCommand.cs
      • NewPersonCommandHandler.cs
      • NewPersonCommandValidator.cs
  • Events
    • NewPersonEvent
      • NewPersonEventHandler.cs
      • NewPersonEvent.cs
  • Queries
    • GetPersonByIdQuery
      • GetPersonByIdQuery.cs
      • GetPersonByIdQueryHandler.cs
      • GetPersonByIdItem.cs
    • GetPeoplePaginatedQuery
      • GetPeopleQueryPaginated.cs
      • GetPeopleQueryPaginatedHandler.cs
      • GetPeopleQueryPaginatedItem.cs

Usage

You can use the AddCqrs extension method to inject and configure the required services in the DI container, passing the Assemblies where the CQRS classes are located (inputs, results, validators and handlers).

builder.Services.AddCqrs(typeof(NewPersonCommandHandler).Assembly);

Commands

Each command scope are composed with:

  • Command
  • CommandValidator
  • CommandHandler

Command

The Command express the input of your command, it's required to implement a specific command input for each command, because it is used by the MeditR to mediate your Command. You must create a Command Input class by implementing the ICommand<TCommandResult>, where the TCommandResult is the result class.

public record NewPersonCommand(string? Name, string? Email, int Age) : ICommand<Guid>;

Command Validator

You can also create an Input Validator using FluentValidation, it will be used in the ValidatonPipeline to automatically validate your command input.

No extra configuration is required, you just need to create the class inheriting from AbstractValidator<TCommand>.

The validatior class is optional.

public class NewPersonCommandValidator : AbstractValidator<NewPersonCommand>
{
    public NewPersonCommandValidator()
    {
        RuleFor(x => x.Name)
            .Cascade(CascadeMode.Stop)
            .NotEmpty()
            .MinimumLength(2)
            .MaximumLength(150);
        
        RuleFor(x => x.Email)
            .Cascade(CascadeMode.Stop)
            .NotEmpty()
            .EmailAddress()
            .MaximumLength(150);

        RuleFor(x => x.Age)
            .GreaterThanOrEqualTo(18);
    }
}

Command Handler

The Command Handler is where your orchestration logic will be created, you could have calls to services, repositories and basicly anything that you need to do in order to complete your command.

Your CommandHandler must implement ICommandHandler<TCommand, TCommandResult>, TCommand been your specific command input and TCommandResult your command result, specific or not.

You must implement the abstract Handle method, this is the method that MediatR will call when you send a Command

public class NewPersonCommandHandler : ICommandHandler<NewPersonCommand, Guid>
{
    private readonly IPersonRepository _personRepository;
    private readonly IMediator _mediator;

    public NewPersonCommandHandler(
        IPersonRepository personRepository,
        IMediator mediator)
    {
        _personRepository = personRepository;
        _mediator = mediator;
    }

    public async Task<Result<Guid>> Handle(NewPersonCommand request, CancellationToken cancellationToken)
    {
        var person = new Person(request.Name!, request.Age);

        _personRepository.AddPerson(person);

        await _mediator.Publish(new NewPersonEvent { PersonId = person.Id }, cancellationToken);

        return Result.Success(person.Id);
    }
}

Queries

The queries follows the same struct as the commands, you have a input, a handler, a result and a validator.

  • Returns a single object: IQuery<TItem>
  • Returns a list of objects: IQuery<IEnumerable<TItem>>
  • Returns a paginated list of objects: IQuery<PaginatedList<TItem>>

The input must implement IQuery<TItem> and may carry filters or any information required to return the result(s).

The queries scope is similar to the command's scope:

  • Query
  • QueryValidator
  • QueryHandler
  • QueryItem

Query

You must create a query input class by implementing IQuery<TQueryResult>, where the TQueryResult is your query result class.

public record GetPersonByIdQuery(Guid Id) : IQuery<GetPersonByIdQueryItem>;

Query Handler

The query handler must implement IQueryHandler<TQuery, TQueryResult>, TQuery been your specific query input and TQueryResult your query result, specific or not.

You must implement the abstract Handle method, this is the method that MediatR will call when you send a Query

public class GetPersonByIdQueryHandler : IQueryHandler<GetPersonByIdQuery, GetPersonByIdQueryItem>
{
    private readonly IPersonRepository _personRepository;

    public GetPersonByIdQueryHandler(IPersonRepository personRepository)
    {
        _personRepository = personRepository;
    }
    
    public async Task<Result<GetPersonByIdQueryItem>> Handle(GetPersonByIdQuery request, CancellationToken cancellationToken)
    {
        var person = _personRepository.GetPeople().FirstOrDefault(x => x.Id == request.Id);
        
        var personResult = person is null
            ? null
            : new GetPersonByIdQueryItem(person.Id, person.Name, person.Age);

        return personResult; // using the implict operator to return 
    }
}

Events

Events works in a fire and forget way.

  • Create a Input that implements IEvent
  • Create a handler that implements from IEventHandler

There is not Validation or Results in Events

Event

public record NewPersonEvent(Guid PersonId) : IEvent;

Event Handler

public class NewPersonEventHandler : IEventHandler<NewPersonEvent>
{
    private readonly ILogger<NewPersonEventHandler> _logger;

    public NewPersonEventHandler(ILogger<NewPersonEventHandler> logger)
    {
        _logger = logger;
    }

    public Task Handle(NewPersonEvent notification, CancellationToken cancellationToken)
    {
        _logger.LogInformation("Person [{PersonId}] created!", notification.PersonId);

        return Task.CompletedTask;
    }
}

Pipelines

Pipeline behaviors is a way that MeditR give us to insert code into the execution pipeline.

When we call IMediator.Send(new FooCommand()), the input will pass throught all the pipelines until it get into the Handler method, and then will return throught them again.

For example, today EasyCqrs has the ValidationPipelineBehavior (you also can create yours):

Controller => ValidationPipeline => any-other-custom-pipeline => Handler

Read more about MediatR Pipeline Behavior

Validation Pipeline

The Validation Pipeline is responsible for retreive all the validators for that input from the DI container, validate the input, and return the errors.

Meaning that if the input has any validation errors, the request will short circuit and return to the caller.

About

A library to work easier with CQRS on top of MediatR.

Resources

Stars

Watchers

Forks

Packages

No packages published