Skip to content

CQRS, Domain-Driven-Design and event-sourcing with ASP.NET Core by example

CQRS and event-sourcing is often talked of as something overly complex that only a selected group of developers can comprehend.

Nowadays, with so many brilliant libraries available that solve most of your problems, I would argue that this is definitely not the case.

It’s been a while since I’ve been wanting to combine CQRS with Either as an error-propagating mechanism so I started to play with that.

Eventually I threw in an event-sourced aggregate and the application grew to what I think is a decentish example of how CQRS with event-sourcing in .NET can be simple, as long as you’re aware of what you’re doing.

Check out the result here.

The application resembles a bar. You can open a tab, order beverages, have your beverages served and pay. Each operation has a corresponding command (OpenTab, OrderBeverages, etc.).

The command handlers are all utilizing the Either monad to do their error propagating, which gives us amazing readability.

Example:

public Task<Option<Unit, Error>> Handle(OrderBeverages request, CancellationToken cancellationToken) =>
    ValidateCommandIsNotEmpty(request).FlatMapAsync(command =>
    GetTabIfNotClosed(command.TabId, cancellationToken).FlatMapAsync(tab =>
    GetBeveragesIfInStock(command.MenuNumbers).MapAsync(beveragesToOrder =>
    PublishEvents(tab.Id, tab.OrderBeverages(beveragesToOrder)))));

public Task<Option<Unit, Error>> Handle(ServeBeverages request, CancellationToken cancellationToken) =>
    ValidateCommandIsNotEmpty(request).FlatMapAsync(command =>
    AssureAllBeveragesAreOutstanding(command, cancellationToken).FlatMapAsync(tab =>
    GetBeveragesIfInStock(command.MenuNumbers).MapAsync(beveragesToServe =>
    PublishEvents(tab.Id, tab.ServeBeverages(beveragesToServe)))));

The menu (the available beverages) is stored in a relational database structure in PostgreSql using EntityFrameworkCore.

The tabs (event-sourced aggregates) are again in PostgreSql, but persisted as streams of events (jsonb) using Marten.

Unneeded abstractions have been avoided (eg. abstracting the IMediator interface) to keep things as simple as possible.

Still, the app is striving to resemble code that is good enough to be put in production and solve real business problems, therefore it has nearly 100% integration test coverage.

[Theory]
[AutoData]
public async Task CanOpenTab(Guid tabId, string clientName)
{
    // Arrange
    var command = new OpenTab
    {
        TabId = tabId,
        ClientName = clientName
    };

    // Act
    var result = await _fixture.SendAsync(command);

    // Assert
    result.HasValue.ShouldBeTrue();

    await AssertTabExists(
        tabId,
        t => t.Id == tabId &&
        t.ClientName == clientName &&
        t.IsOpen == true);
}

[Theory]
[AutoData]
public async Task CanServeOneBeverage(Guid tabId, List<Beverage> beverages)
{
    // Arrange
    await OpenTab(tabId);
    await AddBeveragesToDb(beverages);

    var beverageToServe = beverages.First().MenuNumber;

    // We intentionally order it twice in order to be able to assert that only one was served
    await OrderBeverages(tabId, beverageToServe);
    await OrderBeverages(tabId, beverageToServe);

    // Act
    var result = await _fixture.SendAsync(new ServeBeverages
    {
        TabId = tabId,
        MenuNumbers = new[] { beverageToServe }
    });

    // Assert
    result.HasValue.ShouldBeTrue();

    await AssertTabExists(
        tabId,
        tab => tab.ServedBeverages.Count == 1 && // One served
               tab.ServedBeverages.First().MenuNumber == beverageToServe &&
               tab.OutstandingBeverages.Count == 1 && // And one left, because we oredered it twice
               tab.OutstandingBeverages.First().MenuNumber == beverageToServe);
}

Overall, it’s been pretty fun to write.

Now it’s your turn to review it.

Have fun.

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *