Applying CQRS and Mediator in Asp.Net Core

Photo by Trent Erwin on Unsplash

Applying CQRS and Mediator in Asp.Net Core

This blog post will discuss CQRS and Mediator patterns and apply them in an Asp.Net Core application.

What is CQS?

CQS is a design pattern, and the acronym stands for Command Query Separation. It states that every method should either be a command that executes a task or a query that returns data to the caller, but not both. Methods should return a value only if they create no side effects.

What is CQRS?

Command Query Responsibility Segregation (CQRS) is the segregation of the responsibilities of the commands and queries in a system. It divides a system’s actions into commands and queries, and it is related to CQS.

Because of the flexibility created by migrating to CQRS, the system evolves better over time and prevents update commands from causing merge conflicts at the domain level.

When to use it?

For Martin Fowler: Like any pattern, CQRS is useful in some places but not others. CQRS is a significant mental leap for all concerned, so it should only be tackled if the benefit is worth the jump. In particular, CQRS should only be used on specific portions of a system and not the system as a whole.

CQRS is an excellent pattern to use when the solution has rich/complex business logic.

Also, in scenarios where:

  • performance of data reads must be fine-tuned separately from the performance of data writes.

  • one team of developers can focus on the complex domain model that is part of the write model, and another team can concentrate on the read model and the user interfaces.

  • the system is expected to evolve and might contain multiple versions of the model or where business rules change regularly.

Principles

The core principle of CQRS performs fundamentally different roles within a system. Separating them means that each can be optimized as needed, which can be hugely beneficial for distributed systems.

Commands

A command is an instruction, a directive to perform a specific task. It is an intention to change something and is created when data is to be mutated.

For Alexey Zimarev: handling a command should result in one transaction on one aggregate; basically, each command should clearly state one well-defined change.

Queries

A query is a request for information and is only created when data is to be retrieved from the data store.

It is used to get data or the status of data, and the request should not change the data.

What is Mediator?

In software engineering, the Mediator pattern defines an object that encapsulates how objects interact.

Instead of having two or more objects that take a direct dependency on each other, they interact with a mediator, who is in charge of sending those interactions to the other component/object.

This pattern is considered a behavioural pattern because it can change the program’s running behaviour.

Implementation

First, let’s create a new Asp.Net Core Web API project; in my case, I will make it a minimal API project.

Structure

Before implementing the commands and queries, let's create some structure for the app.

Domain

In the Domain project, it was created the entities that are going to be used for now.

public class Exercise
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int MuscleId { get; set; }
    public virtual Muscle Muscle { get; set; }
}
public class Muscle
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual List<Exercise> Exercises { get; set; }
}

And also the interfaces for the repository and the service.

public interface IMuscleRepository
{
    Task<IEnumerable<Muscle>> GetAll();
    Task<Muscle> GetById(int id);
    Task<int> Create(Muscle muscle);
    Task<int> Update(Muscle muscle);
    Task<int> Delete(Muscle muscle);
}
public interface IMuscleService
{
    Task<IEnumerable<Muscle>> GetAll();
    Task<Muscle> GetById(int id);
    Task<int> Create(Muscle muscle);
    Task<int> Update(Muscle muscle);
    Task<int> Delete(Muscle muscle);
}

Data

For the Data project, the Entity Framework was used as an ORM. The following code shows the implementation of the context.

public class CoachPlanContext : DbContext
{
    public virtual DbSet<Muscle> Muscles { get; set; }
    public virtual DbSet<Exercise> Exercises { get; set; }

    public CoachPlanContext(DbContextOptions<CoachPlanContext> options)
 : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<Exercise>(entity =>
        {
            entity.HasOne(d => d.Muscle)
                .WithMany(d => d.Exercises)
                .HasForeignKey(d => d.MuscleId);

            entity.Property(e => e.Name)
                .IsRequired(true)
                .HasMaxLength(200);

        });

        modelBuilder.Entity<Muscle>(entity =>
        {
            entity.Property(e => e.Name)
                .IsRequired(true)
                .HasMaxLength(200);
        });
    }
}

Also, was created a repository for the entity Muscle.

    public class MuscleRepository : IMuscleRepository
    {
        private readonly CoachPlanContext _coachPlanContext;

        public MuscleRepository(CoachPlanContext coachPlanContext)
        {
            _coachPlanContext = coachPlanContext;
        }

        public async Task<int> Create(Muscle muscle)
        {
            _coachPlanContext.Muscles.Add(muscle);

            return await _coachPlanContext.SaveChangesAsync();
        }

        public async Task<int> Delete(Muscle muscle)
        {
            _coachPlanContext.Remove(muscle);

            return await _coachPlanContext.SaveChangesAsync();
        }

        public async Task<IEnumerable<Muscle>> GetAll()
        {
            return await _coachPlanContext.Muscles.ToListAsync();
        }

        public async Task<Muscle> GetById(int id)
        {
            return await _coachPlanContext.Muscles.Where(m => m.Id == id).SingleOrDefaultAsync();
        }

        public async Task<int> Update(Muscle muscle)
        {
            _coachPlanContext.Muscles.Update(muscle);

            return await _coachPlanContext.SaveChangesAsync();
        }
    }

Service

So far was created only the MuscleService for this project.

public class MuscleService : IMuscleService
{
    private readonly IMuscleRepository _muscleRepository;

    public MuscleService(IMuscleRepository muscleRepository)
    {
        _muscleRepository = muscleRepository;
    }   

    public async Task<int> Create(Muscle muscle)
    {
        return await _muscleRepository.Create(muscle);
    }

    public async Task<int> Delete(Muscle muscle)
    {
        return await _muscleRepository.Delete(muscle);
    }

    public async Task<IEnumerable<Muscle>> GetAll()
    {
        return await _muscleRepository.GetAll();
    }

    public async Task<Muscle> GetById(int id)
    {
        return await _muscleRepository.GetById(id);
    }

    public async Task<int> Update(Muscle muscle)
    {
        return await _muscleRepository.Update(muscle);
    }
}

CQRS

I will create feature-wise folders to implement the Commands and Queries using the CQRS Pattern. All CQRS commands, queries, handlers, and validators related to one feature can be grouped under one folder. For example, to create all our commands and queries related to Muscles, we can have a Muscles folder, and inside that folder, we can have separate folders for Commands and Queries.

Queries

All the queries will implement the IRequest<T> interface available in the MediatR library, and it is necessary to specify what it will return.

Next, we need to define a handler class that implements the IRequestHandler interface. This class can be defined outside the class; I am implementing it as a nested class.

The IMuscleService service is injected into the constructor of GetAllMusclesQuery and GetMuscleByIdQuery.

public class GetAllMusclesQuery : IRequest<IEnumerable<Domain.Entities.Muscle>>
{
    public class GetAllMusclesQueryHandler : IRequestHandler<GetAllMusclesQuery, IEnumerable<Domain.Entities.Muscle>>
    {
        private readonly IMuscleService _muscleService;

        public GetAllMusclesQueryHandler(IMuscleService muscleService)
        {
            _muscleService = muscleService;
        }

        public async Task<IEnumerable<Domain.Entities.Muscle>> Handle(GetAllMusclesQuery query, CancellationToken cancellationToken)
        {
            return await _muscleService.GetAll();
        }
    }
}
public class GetMuscleByIdQuery : IRequest<Domain.Entities.Muscle>
{
    private readonly int id;

    public GetMuscleByIdQuery(int id)
    {
        this.id = id;
    }   
    public class GetMuscleByIdQueryHandler : IRequestHandler<GetMuscleByIdQuery, Domain.Entities.Muscle>
    {
        private readonly IMuscleService _muscleService;

        public GetMuscleByIdQueryHandler(IMuscleService muscleService)
        {
            _muscleService = muscleService;
        }

        public async Task<Domain.Entities.Muscle> Handle(GetMuscleByIdQuery query, CancellationToken cancellationToken)
        {
            return await _muscleService.GetById(query.id);
        }
    }
}

Commands

The commands will follow the same idea as the queries; the difference is that the commands can change the data.

It was created the CreateMuscleCommand, DeleteMuscleCommand and UpdateMuscleCommand and their respective handlers.

Because of the example's simplicity, the domain entity was used as the query return type and as a parameter for the commands. It is a better practice to use DTOs to hide a domain entity from the public API.

public class CreateMuscleCommand : IRequest<Unit>
{
    private readonly Domain.Entities.Muscle muscle;

    public CreateMuscleCommand(Domain.Entities.Muscle muscle)
    {
        this.muscle = muscle;
    }      

    public class CreateMuscleCommandHandler : IRequestHandler<CreateMuscleCommand, Unit>
    {
        private readonly IMuscleService _muscleService;

        public CreateMuscleCommandHandler(IMuscleService muscleService)
        {
            _muscleService = muscleService;
        }

        public async Task<Unit> Handle(CreateMuscleCommand command, CancellationToken cancellationToken)
        {
            await _muscleService.Create(command.muscle);

            return Unit.Value;
        }
    }
}
public class DeleteMuscleCommand : IRequest<Unit>
{
    private readonly int id;

    public DeleteMuscleCommand(int id)
    {
        this.id = id;
    }   

    public class DeleteMuscleCommandHandler : IRequestHandler<DeleteMuscleCommand, Unit>
    {
        private readonly IMuscleService _muscleService;

        public DeleteMuscleCommandHandler(IMuscleService muscleService)
        {
            _muscleService = muscleService;
        }

        public async Task<Unit> Handle(DeleteMuscleCommand command, CancellationToken cancellationToken)
        {
            var muscle = await _muscleService.GetById(command.id);
            if (muscle == null)
                return default;

            await _muscleService.Delete(muscle);

            return Unit.Value;
        }
    }
}
public class UpdateMuscleCommand : IRequest<Unit>
{
    private readonly int id;
    private readonly Domain.Entities.Muscle muscle;

    public UpdateMuscleCommand(int id, Domain.Entities.Muscle muscle)
    {
        this.id = id;
        this.muscle = muscle;
    }

    public class UpdateMuscleCommandHandler : IRequestHandler<UpdateMuscleCommand, Unit>
    {
        private readonly IMuscleService _muscleService;

        public UpdateMuscleCommandHandler(IMuscleService muscleService)
        {
            _muscleService = muscleService;
        }

        public async Task<Unit> Handle(UpdateMuscleCommand command, CancellationToken cancellationToken)
        {
            var muscle = await _muscleService.GetById(command.id);
            if (muscle == null)
                return default;

            muscle.Name = command.muscle.Name; 

            await _muscleService.Update(muscle);

            return Unit.Value;
        }
    }
}

MediatR

The MediatR library describes itself as a “Simple, unambitious mediator implementation in .NET.” All communication between the user interface and the data store happens via MediatR; this helps us to build CQRS systems.

We need to install two nugget packages: MediatR and MediatR.Extensions.Microsoft.DependencyInjection. After that, we can register the MediatR in our application.

MediatR Requests are straightforward request-response style messages where a single handler synchronously handles a single request.

Application

First, it is necessary to register everything the application will use. The following code registers the MediatR, service, repository and the EF DbContext.

builder.Services.AddMediatR(Assembly.GetExecutingAssembly());

builder.Services.AddScoped<IMuscleRepository, MuscleRepository>();
builder.Services.AddScoped<IMuscleService, MuscleService>();

builder.Services.AddDbContext<CoachPlanContext>(options =>
       options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

All the endpoints receive the IMediator instance, and this one will send the request using the correct command or query.

app.MapGet("/muscles", async (IMediator _mediator) =>
{
    return await _mediator.Send(new GetAllMusclesQuery());
});

app.MapGet("/muscles/{id}", async (IMediator _mediator, int id) =>
{
    return await _mediator.Send(new GetMuscleByIdQuery(id));
});

app.MapPost("/muscles", async (IMediator _mediator, Muscle muscle) =>
{
   await _mediator.Send(new CreateMuscleCommand(muscle));

    return Results.Created($"/muscles/{muscle.Id}", muscle);
});

app.MapPut("/muscles/{id}", async (IMediator _mediator, int id, Muscle muscle) =>
{
    await _mediator.Send(new UpdateMuscleCommand(id, muscle));

    return Results.NoContent();
});

app.MapDelete("/muscles/{id}", async (IMediator _mediator, int id) =>
{
    await _mediator.Send(new DeleteMuscleCommand(id));

    return Results.Ok();
});

Wrapping Up

This was a simple example of how to use CQRS and Mediator patterns. More things can be added, like MediatR Behaviors and Notifications. I will keep updating this project.

You can find the complete code on my GitHub.