MinimalCqrs 4.1.0
dotnet add package MinimalCqrs --version 4.1.0
NuGet\Install-Package MinimalCqrs -Version 4.1.0
<PackageReference Include="MinimalCqrs" Version="4.1.0" />
<PackageVersion Include="MinimalCqrs" Version="4.1.0" />
<PackageReference Include="MinimalCqrs" />
paket add MinimalCqrs --version 4.1.0
#r "nuget: MinimalCqrs, 4.1.0"
#:package MinimalCqrs@4.1.0
#addin nuget:?package=MinimalCqrs&version=4.1.0
#tool nuget:?package=MinimalCqrs&version=4.1.0
Minimal CQRS
Formerly known as AstroCQRS
.
https://www.nuget.org/packages/MinimalCqrs
A clean, modern implementation of CQRS (Command Query Responsibility Segregation) and Vertical Slice Architecture, designed to simplify and scale application development in .NET.
Quite possibly the easiest and most beginner-friendly implementation out there.
It is designed to be used with:
- .NET 9 & .NET 8
- Minimal API
- Azure Functions (HttpTrigger, ServiceBusTrigger and TimeTrigger)
- Console app
- Blazor (todo)
- MVC (todo)
Usage
- Install:
dotnet add package MinimalCqrs
- Configure :
builder.Services.AddMinimalCqrs();
Query
- 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!
Command
- 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
Azure Functions
Here are the same query and command used in Azure Functions!
services.AddMinimalCqrsFromAssemblyContaining<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);
}
}
Handlers
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": {}
}
Sample Code
Check samples available in this repo.
Also check a solution template here: Astro Architecture.
Motives
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
Todo
There are few things to work out here and mainly:
- Blazor example
- MVC example
- Unit test example
- Integration test example
- Benchmarks
Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
Questions, feature requests
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- FluentValidation (>= 11.11.0)
- Microsoft.Azure.Functions.Worker (>= 2.0.0)
- Microsoft.Extensions.DependencyInjection (>= 9.0.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.1)
-
net9.0
- FluentValidation (>= 11.11.0)
- Microsoft.Azure.Functions.Worker (>= 2.0.0)
- Microsoft.Extensions.DependencyInjection (>= 9.0.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.