Eventuous 0.14

Alexey Zimarev
Eventuous
Published in
5 min readFeb 10, 2023

--

Today I released the new version of Eventuous with some exciting improvements. The documentation is updated accordingly.

Breaking change: the ApplicationService abstract class is renamed to CommandService. It was too hard to keep it backwards compatible, so you’ll need to search and replace. The rename also touches all the DI extensions like AddApplicationService, which is now AddCommandService.

Functional domain models

First, now there’s a way to avoid using the Aggregate pattern. I know that many people prefer building their applications using more functional approach, where the decisions are made by a set of functions called the Domain Module rather by objects like Aggregates.

Because functional domain models don’t need the Aggregate abstraction, the focus was on creating an easy way to handle commands, and remove as much boilerplate code, similar to what the formerly known ApplicationService abstraction (now renamed to CommandService).

The new version of the command service is called FunctionalService (not the best name, but ok), and it does the following:

  • Loads the stream events if needed
  • Reconstructs the state
  • Calls the domain module function
  • Persists new events

Here is a partial code of a service that uses this new feature:

public class BookingFuncService : FunctionalCommandService<BookingState> {
public BookingFuncService(IEventStore store, TypeMapper? typeMap = null)
: base(store, typeMap) {
// Register command handlers
OnNew<BookRoom>(cmd => GetStream(cmd.BookingId), BookRoom);
OnExisting<RecordPayment>(
cmd => GetStream(cmd.BookingId),
RecordPayment
);

// Helper function to get the stream name from the command
static StreamName GetStream(string id)
=> new StreamName($"Booking-{id}");

// When there's no stream to load, the function only
// receives the command
static IEnumerable<object> BookRoom(BookRoom cmd) {
yield return new RoomBooked(
cmd.RoomId, cmd.CheckIn, cmd.CheckOut, cmd.Price
);
}
...

Hey, no aggregates! Find out more in the documentation.

As Eventuous also provides a way to put an API on top of the command service, I added the same feature for functional services. Also, new DI container extensions are available for the new service type. However, at this moment it is not possible to use generated command API endpoints, it will come later.

Command mapping

One of the issues with the command service is that it will load events from an event store before calling the domain logic. As part of the domain logic could be constructing value objects, it might fail there. In fact, constructing value objects from command data doesn’t require loading events and reconstructing the state, so the flow felt suboptimal. Sending lots of obviously invalid commands could eventually overload the database, and that’s just bad.

The command mapping feature allows separating API contracts (DTOs) from commands handled by the domain model. When these are separated, you can start using value objects in domain commands because the mapping will take care of the impedance mismatch. Because the mapping function is called before the command gets to the command service, it will fail much earlier, and avoid calling the database when the command is invalid.

Another advantages are:

  • Less code in the service as all the value objects are already pre-constructed in the mapper
  • Domain commands can be changed independently from DTOs
  • You can use the same domain commands with different API contracts, like one set of DTOs for HTTP plus contracts fro gRPC generated from proto files
  • Domain commands become first-class citizens of the domain model

Eventuous provides two ways to do this.

When using API controllers, you can use the command mapper combined with the new Handle<TContract, TCommand> function of the controller base class:

public class BookingApi : CommandHttpApiBase<Booking> {
public BookingApi(
ICommandService<Booking> service,
MessageMap? commandMap = null
) : base(service, commandMap) { }

[HttpPost("v2/pay")]
public Task<ActionResult<Result>> RegisterPayment(
[FromBody] RegisterPaymentHttp cmd,
CancellationToken cancellationToken
)
=> Handle<RegisterPaymentHttp, Commands.RecordPayment>(
cmd, cancellationToken);
}

In this example, RegisterPaymentHttp is the contract DTO, and RecordPayment is the domain command. You might notice the MessageMap dependency, which should be constructed and registered in the bootstrap code:

var commandMap = new MessageMap()
.Add<BookingApi.RegisterPaymentHttp, Commands.RecordPayment>(
x => new Commands.RecordPayment(
new BookingId(x.BookingId),
x.PaymentId,
new Money(x.Amount),
x.PaidAt
)
);

builder.Services.AddSingleton(commandMap);

The second option is available when using generated command API endpoints. There, you can use a more advance version of the MapCommand function, which allows to specify the mapping:

var app = builder.Build();

app
.MapAggregateCommands<Booking>
.MapCommand<ProcessPaymentHttp, Commands.ProcessPayment>(
(cmd, ctx) => new Commands.ProcessPayment(
new BookingId(cmd.BookingId), // Create value object from primitive
cmd.PaymentId, // Use primitive
new Money(cmd.Amount),
cmd.PaidAt,
ctx.User.Identity.Name // Use HttpContext to get user details
)
);

Find out more about this feature in the documentation.

Postgres projector

When I was writing documentation about custom Event Handlers, I needed an example. As I suggest using custom handlers to create new projectors, I decided to build one for PostgesSQL. It’s a very simple version, but it works quite well when combined with the PostgresCheckpointStore.

When using this feature you can build read models in PorgreSQL. You can have your write model (events) in any store provided by Eventuous.

Here’s an example of how it can be used:

public class ImportingBookingsProjector : PostgresProjector {
public ImportingBookingsProjector(GetPostgresConnection getConnection)
: base(getConnection) {
const string insert = @"insert into myschema.bookings
(booking_id, checkin_date, price)
values (@booking_id, @checkin_date, @price)";

On<BookingEvents.BookingImported>(
(connection, ctx) =>
Project(
connection,
insert,
new NpgsqlParameter("@booking_id", ctx.Stream.GetId()),
new NpgsqlParameter("@checkin_date", ctx.Message.CheckIn.ToDateTimeUnspecified()),
new NpgsqlParameter("@price", ctx.Message.Price)
)
);
}
}

All you need to do there is to specify what SQL statement will be triggered by what event type.

Here’s how you can register a subscription that would use the ImportingBookingsProjector:

builder.Services.AddSubscription<PostgresAllStreamSubscription, PostgresAllStreamSubscriptionOptions>(
"ImportedBookingsProjections",
builder => builder
.UseCheckpointStore<PostgresCheckpointStore>()
.AddEventHandler<ImportingBookingsProjector>();
);

Read more on the documentation page about Postgres support in Eventuous.

Other things

When implementing those features, I was adding lots of small things in different places. For example, there are new extensions for IEventStore to load events and fold state in one call, append events easier when not using AggregateStore, etc. You’d not normally use those things, but if you need them — check IntelliSense for those interfaces! I also added tracing support for functional services, so everything will be measured for the new service kind as does for the aggregate-based command service.

I migrated the documentation website to Docusaurus. It’s very fast and, most importantly, has a dark theme! When migrating I edited some of the pages and added content to empty pages, but it’s still work in progress for the diagnostics page and infrastructure-specific components like RabbitMQ and Google PubSub. Still, hope you like it.

Eventuous is open source and free to use. If you use it in production, or it helped you learning Event Sourcing, or you just like my work, please consider sponsoring it. 🙏❤️

--

--

Alexey is the Event Sourcing and Domain-Driven Design enthusiast and promoter. He works as a Developer Advocate at Event Store and Chief Architect at ABAX.