DEV Community

Arda
Arda

Posted on

Source Generator-Based CQRS Library in C#: A Review of the Mevora Library

Hello, fellow .NET developers! Today, I’d like to talk to you about a new library—Mevora—that promises to be quite exciting, and how it might encourage us to adjust our habits a bit when using the CQRS and Mediator patterns in the .NET ecosystem.

Why Mevora?

  • Compile-time safety (no missing handlers ever)
  • Source Generator based
  • Native AOT friendly
  • Built-in validation system
  • Pub/Sub support out of the box

Mevora is a highly efficient, compile-time-focused CQRS/Mediator library supported by the C# Source Generators framework. Another key feature of Mevora is that if you define a Request class but forget to define the corresponding Processor class, your project will fail to compile. This is a crucial feature for large-scale projects.

Additionally, the library includes a built-in validation system. You can validate your objects very quickly.

1. Installation and Configuration

dotnet add package Mevora

Next, we’ll add the following code to our Program.cs file:

using Mevora;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddMevora(cfg =>
{
   // Automatically scans all Processors, Validators, and Handlers in your project
    cfg.AddProcessorsFromAssembly(typeof(Program).Assembly);

    // (Optional) You can add your own pipelines here in sequence
    // cfg.AddPipelineAction(typeof(LoggingPipeline<,>));
});

// Finally, we include IMevoraDispatcher in our project
builder.Services.AddMevoraDispatcher();

var app = builder.Build();

Enter fullscreen mode Exit fullscreen mode

Mevora is now ready for use in our project!

2. Requests and Processors

In Mevora, objects that carry data are called Request, while objects that execute business logic are called Processor. Let’s perform a simple user retrieval task.

First, we define the class required for the request:

public class GetUserRequest : IRequest<UserDto>
{
    public Guid UserId { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Next, we write the Processor class that will handle the request:

public class GetUserRequestProcessor : IRequestProcessorAsync<GetUserRequest, UserDto>
{
    private readonly IUserRepository _repository;

    public GetUserRequestProcessor(IUserRepository repository)
    {
        _repository = repository;
    }

    public async Task<UserDto> ProcessAsync(GetUserRequest request, CancellationToken cancellationToken)
    {
        var user = await _repository.GetUserAsync(request.UserId, cancellationToken);
        return new UserDto(user.Id, user.Name);
    }
}
Enter fullscreen mode Exit fullscreen mode

Next, we need to pass the relevant objects to our Processor class. We do this as follows:

[ApiController]
[Route("api/users")]
public class UsersController : ControllerBase
{
    private readonly IMevoraDispatcher _dispatcher;

    public UsersController(IMevoraDispatcher dispatcher)
    {
        _dispatcher = dispatcher;
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> GetUser(Guid id, CancellationToken ct)
    {
        var request = new GetUserRequest { UserId = id };

        UserDto result = await _dispatcher.DispatchAsync(request, ct);

        return Ok(result);
    }
}
Enter fullscreen mode Exit fullscreen mode

Another feature that sets Mevora apart from its competitors is its built-in verification system. Let’s verify our user request using the built-in verification system.

public class CreateUserRequestValidator : IRequestValidator<CreateUserRequest>
{
    public ValidationResult Validate(ValidationContext<CreateUserRequest> context)
    {
        context.CheckNotEmpty(r => r.Username, "The username cannot be empty.")
               .CheckMinLength(r => r.Username, 3, "The username must be at least 3 characters long.")
               .CheckNotEmpty(r => r.Email, "The email field is required.");

        return context.Result;
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Event Management with the Pub/Sub Architecture

We’re not limited to just the Request/Response cycle. With Mevora, you can also easily handle your “Event Publishing” operations. If you want to trigger an event and have multiple listeners (Processors) process it simultaneously, we use the IMessage interface.

For example, when an order is created, we want to both send a confirmation email and update the inventory:

public class OrderCreatedMessage : IMessage
{
    public Guid OrderId { get; set; }
}

// 1. Listener: Email Delivery
public class SendEmailOnOrderCreated : IMessageProcessor<OrderCreatedMessage>
{
    public async Task Run(OrderCreatedMessage message, CancellationToken cancellationToken)
    {
        Console.WriteLine($"An email is being sent for order {message.OrderId}...");
    }
}

// 2. Listener: Stock Update
public class UpdateInventoryOnOrderCreated : IMessageProcessor<OrderCreatedMessage>
{
    public async Task Run(OrderCreatedMessage message, CancellationToken cancellationToken)
    {
        Console.WriteLine($"Stocks are being updated for order {message.OrderId}...");
    }
}
Enter fullscreen mode Exit fullscreen mode

Broadcasting this message to the entire system takes just one line of code:

await _dispatcher.PublishAsync(new OrderCreatedMessage { OrderId = order.Id }, cancellationToken);

Final Thoughts

Mevora is a modern library that fully leverages the benefits of the “Source Generator” technology introduced in C# 9 and later, prioritizing architectural safety above all else. With its guaranteed compile-time error detection, native AOT support, and built-in features that reduce external package dependencies, it definitely deserves a chance in modern .NET projects.

If you’re about to lay the groundwork for a new project or are looking for a high-performance solution for CQRS in your existing application, be sure to add Mevora to your toolkit.

To explore more details and support the developer, visit the Mevora GitHub repository and star the project.

Top comments (0)