DEV Community

Artur Kedzior
Artur Kedzior

Posted on

Building .NET 8 APIs with Zero-Setup CQRS and Vertical Slice Architecture

The goal of this article is to introduce you to developer friendly way of building Web APIs and Azure Functions with .NET 8 applying CQRS and Vertical Slice Architecture.

After years of dealing with all sorts of systems Vertical Slice Architecture has become our only way of building web applications.

We will be exploring the open source library AstroCQRS which purpose is to provide zero-setup / out of the box integration.

Let's jump right into an example and create MinimalApi endpoint that fetches order by id in 3 steps:

  1. Install and register AstroCQRS in MinimalAPI
dotnet add package AstroCqrs  
Enter fullscreen mode Exit fullscreen mode
builder.Services.AddAstroCqrs();
Enter fullscreen mode Exit fullscreen mode
  1. Create an endpoint
app.MapGetHandler<GetOrderById.Query, GetOrderById.Response>
("/orders/{id}");
Enter fullscreen mode Exit fullscreen mode
  1. Create a query handler:
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));
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In a single file we have everything it is required to know about this particular feature:

  • request
  • response
  • handler

You could easily skip Response and do:

public class Query : IQuery<IHandlerResponse<OrderModel>>
{
    public string Id { get; set; } = "";
}
Enter fullscreen mode Exit fullscreen mode
app.MapGetHandler<GetOrderById.Query, GetOrderById.OrderModel>
("/orders/{id}");
Enter fullscreen mode Exit fullscreen mode

However I like wrapping response model into Response root model as later I can easily add new properties without modifying the endpoint and changing much on the client consuming that API.

Ok so what the hell is IHandlerResponse?

It serves two purposes:

  1. It enforces consistency with returning either Success or Error.
  2. Internally it provides a way for callers such as Minimal API and Azure Functions to understand the response from a handler and pass it through.

Anyway, going back to the main subject:

Now let's say you want to do the same but with Azure Functions. All you need is a single line:

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Yes, your handler doesn't and shouldn't care who calls it!

Now let's see how we would do order creation:

app.MapPostHandler<CreateOrder.Command, CreateOrder.Response>
("/orders.create");
Enter fullscreen mode Exit fullscreen mode
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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Here additionally we use built-in Fluent Validation.

What about testing?

Ok let's now unit test GetOrderById. You can completely skip firing up API or Azure Functions, setting up http client , deal with authorization and start slapping your handler directly left and right 💥.

[Fact]
public async Task GetOrderById_WhenOrderFound_ReturnsOrder()
{

    var query = new GetOrderById.Query { Id = "1" };
    var handler = new GetOrderById.Handler();

    var expected = new OrderModel("1", "Gavin Belson", 20)();
    var result = await handler.ExecuteAsync(query);

    Assert.NotNull(result.Payload.Order);
    Assert.Equal(expected.Id, result.Payload.Order.Id);
    Assert.Equal(expected.CustomerName, result.Payload.Order.CustomerName);
    Assert.Equal(expected.Total, result.Payload.Order.Total);
}
Enter fullscreen mode Exit fullscreen mode

Check more examples here:
https://github.com/kedzior-io/astro-cqrs/tree/main/samples

We are using it in production here:

Top comments (3)

Collapse
 
chrisrlewis profile image
Chris Lewis • Edited

Thanks for sharing.
We routinely use Mediatr, can you expand on what the differences are with AstroCQRS specifically for Azure Functions?
Do you have the equivalent of Mediatr cross-cutting pipeline behaviours?

Collapse
 
kedzior_io profile image
Artur Kedzior

They are very similar but AstroCQRS goes little further abstracting as much as possible in order to make it zero setup and shift developer's focus on business requirements. I read often that developers don't use Mediator because of the overhead with setting it up.

For example in AstroCQRS there is no need to inject IMediator nor explicitly call Send, you just map Minimal API endpoint to the handler and that's all. Same with Azure Functions, you map them directly to a handler. API and Azure Functions become single liners and so the code of each lives in a single handler (input, handler, response), easily findable and easily unit testable. No need to deal with spinning API's nor Functions and dealing with all that comes with it (JWT tokens, setting up headers etc.). Each handler by default allows an easy access to DbContext, RequestContext (claims, headers, languages etc) and logging. Each handler also injects and runs validators by default.

All in all it provides a developer with most of the things necessary to develop Web API or Azure Functions.

AstroCQRS also focuses on providing the best performance possible.

Unfortunately there are no pipeline behaviours supported yet. I would love to find a good use case for needing one though. Most examples I have seen is for validation (which is already builtin AstroCQRS) and global logging which I would never do myself.

I would be happy to add it though if requested here github.com/kedzior-io/astro-cqrs

Collapse
 
jangelodev profile image
João Angelo

Artur Kedzior, great post !
Thanks for sharing