AstroCqrs 3.0.2
AstroCqrs has been renamed to MinimalCqrs
dotnet add package AstroCqrs --version 3.0.2
NuGet\Install-Package AstroCqrs -Version 3.0.2
<PackageReference Include="AstroCqrs" Version="3.0.2" />
<PackageVersion Include="AstroCqrs" Version="3.0.2" />
<PackageReference Include="AstroCqrs" />
paket add AstroCqrs --version 3.0.2
#r "nuget: AstroCqrs, 3.0.2"
#:package AstroCqrs@3.0.2
#addin nuget:?package=AstroCqrs&version=3.0.2
#tool nuget:?package=AstroCqrs&version=3.0.2
Astro CQRS
https://www.nuget.org/packages/AstroCqrs

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
- Minimal API
- Azure Functions (HttpTrigger, ServiceBusTrigger and TimeTrigger)
- Console app
- Blazor (todo)
- MVC (todo)
Usage
- Install:
dotnet add package AstroCqrs
- Configure :
builder.Services.AddAstroCqrs();
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.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);
}
}
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.
| Version | Downloads | Last Updated | |
|---|---|---|---|
| 3.0.2 | 429 | 5/12/2025 | |
| 3.0.1 | 379 | 3/3/2025 | |
| 3.0.0 | 259 | 1/27/2025 | |
| 2.0.6 | 5,987 | 1/17/2024 | |
| 2.0.5 | 254 | 1/17/2024 | |
| 2.0.4 | 286 | 1/16/2024 | |
| 2.0.3 | 267 | 1/16/2024 | |
| 2.0.2 | 277 | 1/12/2024 | |
| 2.0.1 | 293 | 1/3/2024 | |
| 2.0.0 | 285 | 1/2/2024 | |
| 1.0.1 | 272 | 1/1/2024 | |
| 1.0.0 | 293 | 12/21/2023 | |
| 1.0.0-CI-20240101-211748 | 269 | 1/1/2024 |