RO EN

Implementing CQRS bases in .NET

Implementing CQRS bases in .NET
Doru Bulubasa
09 July 2025

After understanding the basic concepts and the recommended structure, it's time to move on to implementation. At this stage, we will configure MediatR and create the first commands and queries in a .NET application, using a simple example: Post.


3๏ธโƒฃ Installing MediatR

To separate responsibilities and avoid direct ties between layers, we use MediatR โ€“ a library that facilitates sending commands and queries to dedicated handlers.

๐Ÿ› ๏ธ Installation

In the MyApp.Application project, add MediatR:

dotnet add package MediatR

dotnet add package MediatR.Extensions.Microsoft.DependencyInjection

In MyApp.API (or the UI/API project), configure the services in Program.cs:

builder.Services.AddMediatR(cfg =>

    cfg.RegisterServicesFromAssembly(typeof(MyApp.Application.AssemblyReference).Assembly));

Add a reference class in MyApp.Application:

namespace MyApp.Application;

public static class AssemblyReference { }


4๏ธโƒฃ Creating a simple command: CreatePostCommand

๐Ÿงพ Command: CreatePostCommand.cs

using MediatR;

public record CreatePostCommand(string Title, string Content) : IRequest<Guid>;

IRequest<Guid> means that this command will return a Guid (the ID of the created post)


๐Ÿง  Handler: CreatePostCommandHandler.cs

public class CreatePostCommandHandler : IRequestHandler<CreatePostCommand, Guid>
{
    private readonly IApplicationDbContext _context;

    public CreatePostCommandHandler(IApplicationDbContext context)
    {
        _context = context;
    }

    public async Task<Guid> Handle(CreatePostCommand request, CancellationToken cancellationToken)
    {
        var post = new Post
        {
            Id = Guid.NewGuid(),
            Title = request.Title,
            Content = request.Content,
            CreatedAt = DateTime.UtcNow
        };

        _context.Posts.Add(post);
        await _context.SaveChangesAsync(cancellationToken);

        return post.Id;
    }
}


5๏ธโƒฃ Complete separation for a query: GetPostByIdQuery

๐Ÿ” Query: GetPostByIdQuery.cs

using MediatR;

public record GetPostByIdQuery(Guid Id) : IRequest<PostDto>;

PostDto is a display-only object โ€“ without business logic.


๐Ÿ”„ Handler: GetPostByIdQueryHandler.cs

public class GetPostByIdQueryHandler : IRequestHandler<GetPostByIdQuery, PostDto?>
{
    private readonly IApplicationDbContext _context;

    public GetPostByIdQueryHandler(IApplicationDbContext context)
    {
        _context = context;
    }

    public async Task<PostDto?> Handle(GetPostByIdQuery request, CancellationToken cancellationToken)
    {
        var post = await _context.Posts
            .AsNoTracking()
            .Where(p => p.Id == request.Id)
            .Select(p => new PostDto(p.Id, p.Title, p.Content, p.CreatedAt))
            .FirstOrDefaultAsync(cancellationToken);

        return post;
    }
}


๐Ÿ“ฆ PostDto.cs (in Application)

public record PostDto(Guid Id, string Title, string Content, DateTime CreatedAt);


6๏ธโƒฃ Summary: IRequest and IRequest

Request type

Interface

Returns

Command (write)

IRequest<Unit>

Nothing (Unit.Value)

Command with result

IRequest<Guid>

For example: the created ID

Query (read)

IRequest<PostDto>

A DTO type result


๐Ÿงช Usage example from Controller / UI

[HttpPost]
public async Task<IActionResult> Create(CreatePostCommand command)
{
    var id = await _mediator.Send(command);
    return CreatedAtAction(nameof(GetById), new { id }, null);
}

[HttpGet("{id}")]
public async Task<ActionResult<PostDto>> GetById(Guid id)
{
    var post = await _mediator.Send(new GetPostByIdQuery(id));
    return post is not null ? Ok(post) : NotFound();
}


๐Ÿง  Conclusion

At this stage, we have implemented the first essential pieces of a CQRS system:

  • โœ… Commands that modify the application state

  • โœ… Separate queries for reading

  • โœ… Dedicated and independent handlers

  • โœ… MediatR as a central intermediary

๐Ÿ What's next

In the next article we will add:

  • โœ… Validations with FluentValidation

  • โœ… Mapping between DTOs and entities

  • โœ… Clear separation between WriteModel and ReadModel