Skip to content

kedzior-io/astro-cqrs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

48 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Astro CQRS

Nuget Build Status

Astro.CQRS

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)

Usage

  1. Install:
dotnet add package AstroCqrs  
  1. Configure :
builder.Services.AddAstroCqrs();

Query

  1. 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.

  1. 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

  1. Create an endpoint:
app.MapPostHandler<CreateOrder.Command, CreateOrder.Response>("/orders.create");
  1. 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 response
  • Success() - handler executed with success but has no response
  • Error("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.

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

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages