Skip to content

ArdaByk/Mevora

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

36 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Mevora Logo

Mevora

Mevora is a high-performance, compile-time powered CQRS and Mediator framework for .NET.

By leveraging C# Source Generators, Mevora analyzes your project at compile-time and dynamically generates the dispatcher classes. This eliminates the heavy runtime reflection overhead typically seen in traditional mediator frameworks, making Mevora blazingly fast, Native AOT friendly, and strictly type-safe.


Key Features

  • Compile-Time Safety: If you forget to write a handler for a request, you get a compiler error immediately, not a runtime crash!
  • First-Class Validation: Built-in support for performant, object-pooled validation pipelines.
  • Matryoshka Pipelines: Easily inject behaviors (Caching, Logging, Transaction Handling) around your request processors.
  • Dependency Lifecycle Aware: Full support for Transient, Scoped, and Singleton handlers without captive dependency leaks.
  • Event Publishing: Publish-Subscribe architecture out of the box for handling domain events.

Installation

You can install Mevora via the NuGet Package Manager or the .NET CLI.

NuGet Package Manager Console:

Install-Package Mevora

.NET CLI:

dotnet add package Mevora

Getting Started

To start using Mevora, you need to register it with the ASP.NET Core (or generic Host) completely natively using Dependency Injection.

1. Configuration

In your Program.cs or Startup.cs, register the processors and the dispatcher.

using Mevora;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddMevora(cfg =>
{
    // Scan assemblies to find Processors, Handlers, and Validators
    cfg.AddProcessorsFromAssembly(typeof(Program).Assembly);
    
    // (Optional) Register pipelines in the order you want them to execute
    cfg.AddPipelineAction(typeof(LoggingPipeline<,>));
    
    // (Optional) Change default handler lifetime (Transient is default)
    // cfg.WithServiceLifetime(ServiceLifetime.Scoped); 
});

// Finally, build and register the IMevoraDispatcher
builder.Services.AddMevoraDispatcher();

var app = builder.Build();

Requests & Processors (CQRS)

Mevora splits operations into Requests (which convey the data) and Processors (which contain the business logic). There are two types of requests: those that return a value, and those that do not.

Requests with a Response

Define a request by implementing IRequest<TResponse>.

public class GetUserRequest : IRequest<UserDTO>
{
    public Guid Id { get; set; }
}

Next, implement the matching processor using IRequestProcessorAsync<TRequest, TResponse>.

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);
    }
}

Dispatching Requests

Mevora will automatically discover your GetUserRequestProcessor! To dispatch the request, simply inject IMevoraDispatcher and call DispatchAsync.

[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(id);
        
        // This is type-safe and incredibly fast!
        UserDto result = await _dispatcher.DispatchAsync(request, ct);
        
        return Ok(result);
    }
}

Note: If you create a GetUserDetailsRequest but forget to create the matching processor, your project will refuse to compile. Mevora strongly protects your architecture!


Event Publishing (Pub/Sub)

For logic that involves notifying multiple parts of your system without waiting for a single response, Mevora provides an Event Publishing system via IMessage.

Defining & Processing Messages

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

// Handler 1: Sends an email
public class SendOrderConfirmationEmail : IMessageProcessor<OrderCreatedMessage>
{
    public async Task Run(OrderCreatedMessage message, CancellationToken cancellationToken)
    {
        Console.WriteLine($"Sending email for Order {message.OrderId}");
    }
}

// Handler 2: Updates Inventory
public class UpdateInventoryOnOrderCreated : IMessageProcessor<OrderCreatedMessage>
{
    public async Task Run(OrderCreatedMessage message, CancellationToken cancellationToken)
    {
        Console.WriteLine($"Reducing inventory for Order {message.OrderId}");
    }
}

Publishing Messages

Use the PublishAsync method to trigger all registered processors simultaneously.

await _dispatcher.PublishAsync(new OrderCreatedMessage(order.Id), cancellationToken);

Pipeline Actions (Onion Architecture)

Mevora supports pipeline behaviors that wrap your core processors. This is perfect for cross-cutting concerns like logging, caching, metrics, or database transactions.

Pipeline actions are registered sequentially and execute in a matryoshka (onion) fashion.

Example: A Logging Pipeline

public class LoggingPipeline<TRequest, TResponse> : IPipelineAction<TRequest, TResponse> 
    where TRequest : IRequest<TResponse>
{
    private readonly ILogger<LoggingPipeline<TRequest, TResponse>> _logger;

    public LoggingPipeline(ILogger<LoggingPipeline<TRequest, TResponse>> logger)
    {
        _logger = logger;
    }

    public async Task<TResponse> Run(
        TRequest request, 
        ProcessorDelegate<TResponse> next, 
        CancellationToken cancellationToken)
    {
        _logger.LogInformation($"Starting Request: {typeof(TRequest).Name}");
        
        var stopwatch = Stopwatch.StartNew();
        
        // Execute the next pipeline in the chain, or the actual Processor itself
        var response = await next();
        
        stopwatch.Stop();
        
        _logger.LogInformation($"Completed Request: {typeof(TRequest).Name} in {stopwatch.ElapsedMilliseconds}ms");
        
        return response;
    }
}

Make sure to register your pipeline in the AddMevora configuration!

cfg.AddPipelineAction(typeof(LoggingPipeline<,>));

Built-in Validation System

Mevora comes with an ultra-fast, object-pooled validation system. Before a request even reaches the processor or pipelines, it is validated. If it fails, a ValidationException is thrown, short-circuiting the entire process.

Creating a Validator

To validate a request, implement IRequestValidator<TRequest>. The ValidationContext provides fluent methods out-of-the-box (CheckNotEmpty, CheckMinLength, CheckRange, etc.).

public class CreateUserRequestValidator : IRequestValidator<CreateUserRequest>
{
    public ValidationResult Validate(ValidationContext<CreateUserRequest> context)
    {
        context.CheckNotEmpty(r => r.Username, "Username cannot be empty")
               .CheckMinLength(r => r.Username, 3, "Username must be at least 3 characters")
               .CheckNotEmpty(r => r.Email, "Email must be provided");
               
        return context.Result;
    }
}

Validators are seamlessly cached, and ValidationContext objects are concurrently pooled, meaning the GC (Garbage Collector) receives near-zero overhead during high-traffic validation.


About

A helper library that facilitates the implementation of the mediator design pattern.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages