Building .NET 8 APIs with Zero-Setup CQRS and Vertical Slice Architecture
Tags: csharp, webdev, programming, dotnet
Published: 2024-01-08 22:54:20

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](https://martinfowler.com/bliki/CQRS.html) and [Vertical Slice Architecture](https://www.jimmybogard.com/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](https://github.com/kedzior-io/astro-cqrs) 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

```csharp
dotnet add package AstroCqrs  
```

```csharp
builder.Services.AddAstroCqrs();
```

2. Create an endpoint

```csharp
app.MapGetHandler<GetOrderById.Query, GetOrderById.Response>
("/orders/{id}");
```

3. Create a query handler:

```csharp
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));
        }
    }
}
```

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: 

```csharp
public class Query : IQuery<IHandlerResponse<OrderModel>>
{
    public string Id { get; set; } = "";
}
```
```csharp
app.MapGetHandler<GetOrderById.Query, GetOrderById.OrderModel>
("/orders/{id}");
```
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:

```csharp
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);
    }
}
```

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

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

```csharp
app.MapPostHandler<CreateOrder.Command, CreateOrder.Response>
("/orders.create");
```

```csharp
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);
        }
    }
}
```
Here additionally we use built-in [Fluent Validation](https://docs.fluentvalidation.net/en/latest/). 

## 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 :boom:.   

```csharp
[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);
}
```

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

We are using it in production here: 
- [salarioo.com](https://salarioo.com)
- [Fiz: Groceries in minutes](https://apps.apple.com/kw/app/fiz-groceries-in-minutes/id1575127077)
- [Bilbayt] (https://apps.apple.com/kw/app/bilbayt-food-catering/id1052726070) (currently migrating to it)