Astro CQRS is a developer friendly alternative mediator implementation to MediatR.
In-process messaging with no dependencies that allows to take decoupled, command driven approach.
It is designed to be used with:
- .NET 8
- Minimal API
- Azure Functions (HttpTrigger, ServiceBusTrigger and TimeTrigger)
- Blazor (todo)
- Console app (todo)
- MVC (todo)
- Install:
dotnet add package AstroCqrs
- Configure :
builder.Services.AddAstroCqrs();
- Create an endpoint:
app.MapGetHandler<GetOrderById.Query, GetOrderById.Response>("/orders.getById.{id}");
☝️ Simple: we are telling what's the input, the output and the path.
- Create a
Query
public static class GetOrderById
{
public class Query : IQuery<IHandlerResponse<Response>>
{
public string Id { get; set; } = "";
}
public record Response(OrderModel Order);
public record OrderModel(string Id, string CustomerName, decimal Total);
public class Handler : QueryHandler<Query, Response>
{
public Handler()
{
}
public override async Task<IHandlerResponse<Response>> ExecuteAsync(Query query, CancellationToken ct)
{
// retrive data from data store
var order = await Task.FromResult(new OrderModel(query.Id, "Gavin Belson", 20));
if (order is null)
{
return Error("Order not found");
}
return Success(new Response(order));
}
}
}
☝️ Simple: We keep the input, the output and executing method in a single file (not mandatory though).
.... and that's it!
- Create an endpoint:
app.MapPostHandler<CreateOrder.Command, CreateOrder.Response>("/orders.create");
- Create a
Command
:
public static class CreateOrder
{
public sealed record Command(string CustomerName, decimal Total) : ICommand<IHandlerResponse<Response>>;
public sealed record Response(Guid OrderId, string SomeValue);
public sealed class CreateOrderValidator : Validator<Command>
{
public CreateOrderValidator()
{
RuleFor(x => x.CustomerName)
.NotNull()
.NotEmpty();
}
}
public sealed class Handler : CommandHandler<Command, Response>
{
public Handler()
{
}
public override async Task<IHandlerResponse<Response>> ExecuteAsync(Command command, CancellationToken ct)
{
var orderId = await Task.FromResult(Guid.NewGuid());
var response = new Response(orderId, $"{command.CustomerName}");
return Success(response);
}
}
}
☝️ Simple: Same as above + the command can be flexible and return a response
Here are the same query and command used in Azure Functions!
services.AddAstroCqrsFromAssemblyContaining<ListOrders.Query>();
☝️ Ah yeah, due to the nature of Azure Functions, we need to point to the assembly where the handlers live
public class HttpTriggerFunction
{
[Function(nameof(HttpTriggerFunction))]
public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Anonymous,"get")] HttpRequestData req)
{
return await AzureFunction.ExecuteHttpGetAsync<GetOrderById.Query, GetOrderById.Response>(req);
}
}
public class ServiceBusFunction
{
[Function(nameof(ServiceBusFunction))]
public async Task Run([ServiceBusTrigger("created-order", Connection = "ConnectionStrings:ServiceBus")] string json, FunctionContext context)
{
await AzureFunction.ExecuteServiceBusAsync<CreateOrder.Command, CreateOrder.Response>(json, JsonOptions.Defaults, context);
}
}
The handler always returns three types of responses, which enforce consistency:
Success(payload)
- handler executed with success and has responseSuccess()
- handler executed with success but has no responseError("Error message")
- handler has an error
Error("Order not found")
will return Problem Details (a standard way of specifying errors in HTTP API responses)
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "Order not found",
"status": 400,
"errors": {}
}
Check samples available in this repo.
I'm a big fan of:
and used them all in production environment. So why this?
Well, in all of them I was missing something:
MediatR
- a bit of setup + always abstracted a lot in my own wrappers.Wolverine
- it covers a lot more that I need, it uses a lot of dependencies, has an odd way to setup query handler.FastEndpoints
- its command bus is amazing but the whole library enforces REPR Design Pattern (Request-Endpoint-Response) which I'm not a big fan of. It also doesn't work for Azure Functions or Blazor.
I decided to borrow the best features from existing frameworks to create an in-process messaging mechanism that features:
- Easy setup
- Decoupled and reusable handlers
- Enforced consistency
- Built-in validation
- Out-of-the-box compatibility with multiple project types (including Minimal API, Azure Functions, Console, MVC, Blazor)
- Unit testability
It can be seen in production here: Salarioo.com
There are few things to work out here and mainly:
- Blazor example
- MVC example
- Unit test example
- Integration test example
- Benchmarks
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.