This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Application

Application layer and service

The application layer sits between the system edge (different APIs), and the domain model. It is responsible for handling commands coming from the edge.

Concept

In general, the command handling flow can be described like this:

  1. The edge receives a command via its API (HTTP, gRPC, SignalR, messaging, etc).
  2. It passes the command over to the application service. As the edge is responsible for authentication and some authorisation, it can enrich commands with user credentials.
  3. The command service, which is agnostic to the API itself, handles the command and gives a response to the edge (positive or negative).
  4. The API layer then returns the response to the calling party.

Eventuous gives you a base class to implement command services in the application layer: the ApplicationService.

1 - Application service

Application service and unit of work

Concept

The command service itself performs the following operations when handling one command:

  1. Extract the aggregate id from the command, if necessary.
  2. Instantiate all the necessary value objects. This could effectively reject the command if value objects cannot be constructed. The command service could also load some other aggregates, or any other information, which is needed to execute the command but won’t change state.
  3. If the command expects to operate on an existing aggregate instance, this instance gets loaded from the Aggregate Store.
  4. Execute an operation on the loaded (or new) aggregate, using values from the command, and the constructed value objects.
  5. The aggregate either performs the operation and changes its state by producing new events, or rejects the operation.
  6. If the operation was successful, the service persists new events to the store. Otherwise, it returns a failure to the edge.

Application service base class

Eventuous provides a base class for you to build command services. It is a generic abstract class, which is typed to the aggregate type. You should create your own implementation of a command service for each aggregate type. As command execution is transactional, it can only operate on a single aggregate instance, and, logically, only one aggregate type.

Registering command handlers

We have three methods, which you call in your class constructor to register the command handlers:

Function What’s it for
OnNew Registers the handler, which expects no instance aggregate to exist (create, register, initialise, etc).
It will get a new aggregate instance. The operation will fail when it will try storing the aggregate state due to version mismatch.
OnExisting Registers the handler, which expect an aggregate instance to exist.
You need to provide a function to extract the aggregate id from the command.
The handler will get the aggregate instance loaded from the store, and will throw if there’s no aggregate to load.
OnAny Used for handlers, which can operate both on new and existing aggregate instances.
The command service will try to load the aggregate, but won’t throw if the load fails, and will pass a new instance instead.

Here is an example of a command service form our test project:

public class BookingService
  : ApplicationService<Booking, BookingState, BookingId> {
    public BookingService(IAggregateStore store) : base(store) {
        OnNew<Commands.BookRoom>(
            (booking, cmd)
                => booking.BookRoom(
                    new BookingId(cmd.BookingId),
                    cmd.RoomId,
                    new StayPeriod(cmd.CheckIn, cmd.CheckOut),
                    cmd.Price,
                    cmd.BookedBy,
                    cmd.BookedAt
                )
        );

        OnAny<Commands.ImportBooking>(
            cmd => new BookingId(cmd.BookingId),
            (booking, cmd)
                => booking.Import(
                    new BookingId(cmd.BookingId),
                    cmd.RoomId,
                    new StayPeriod(cmd.CheckIn, cmd.CheckOut)
                )
        );
    }
}

You pass the command handler as a function to one of those methods. The function can be inline, like in the example, or it could be a method in the command service class.

In addition, OnAny and OnExisting need a function, which extracts the aggregate id from the command, as both of those methods will try loading the aggregate instance from the store.

Async command handlers

If you need to get outside your process boundary when handling a command, you most probably would need to execute an asynchronous call to something like an external HTTP API or a database. For those cases you need to use async overloads:

  • OnNewAsync
  • OnExistingAsync
  • OnAnyAsync

These overloads are identical to sync functions, but the command handler function needs to return Task, so it can be awaited.

Result

The command service will return an instance of Result.

It could be an OkResult, which contains the new aggregate state and the list of new events. You use the data in the result to pass it over to the caller, if needed.

If the operation was not successful, the command service will return an instance of ErrorResult that contains the error message and the exception details.

Bootstrap

If you registered the EsdbEventStore and the AggregateStore in your Startup as described on the Aggregate store page, you can also register the application service:

services.AddApplicationService<BookingCommandService, Booking>();

The AddApplicationService extension will register the BookingService, and also as IApplicationService<Booking>, as a singleton. Remember that all the DI extensions are part of the Eventuous.AspNetCore NuGet package.

When you also use AddControllers, you get the command service injected to your controllers.

You can simplify your application and avoid creating HTTP endpoints explicitly (as controllers or minimal API endpoints) if you use the command API feature.

Application HTTP API

The most common use case is to connect the application service to an HTTP API.

Read the Command API feature documentation for more details.

2 - Command API

Auto-generated HTTP API for command handling

Controller base

When using an application service from an HTTP controller, you’d usually inject the service as a dependency, and call it’s Handle method using the request body:

[Route("/booking")]
public class CommandApi : ConteollerBase {
    IApiService<Booking> _service;

    public CommandApi(IApplicationService<Booking> service) => _service = service;

    [HttpPost]
    [Route("book")]
    public async Task<ActionResult<Result>> BookRoom(
        [FromBody] BookRoom cmd, 
        CancellationToken cancellationToken
    ) {
        var result = await _service.Handle(cmd, cancellationToken);
        result Ok(result);
    }
}

The issue here is there’s no way to know if the command was successful or not. As the application service won’t throw an exception if the command fails, we can’t return an error via the HTTP response, unless we parse the result and return a meaningful HTTP response.

Eventuous allows you to simplify the command handling in the API controller by providing a CommandHttpApiBase<TAggregate> abstract class, which implements the ControllerBase and contains the Handle method. The class takes IApplicationService<TAggregate> as a dependency. The Handle method will call the application service, and also convert the handling result to ActionResult<Result>. Here are the rules for exception handling:

Result exception HTTP response
OptimisticConcurrencyException Conflict
AggregateNotFoundException NotFound
Any other exception BadRequest

Here is an example of a command API controller:

[Route("/booking")]
public class CommandApi : CommandHttpApiBase<Booking> {
    public CommandApi(IApplicationService<Booking> service) : base(service) { }

    [HttpPost]
    [Route("book")]
    public Task<ActionResult<Result>> BookRoom(
        [FromBody] BookRoom cmd, 
        CancellationToken cancellationToken
    ) => Handle(cmd, cancellationToken);
}

We recommend using the CommandHttpApiBase class when you want to handle commands using the HTTP API.

Generated command API

Eventuous can use your application service to generate a command API. Such an API will accept JSON models matching the application service command contracts, and pass those commands as-is to the application service. This feature removes the need to create API endpoints manually using controllers or .NET minimal API.

To use generated APIs, you need to add Eventuous.AspNetCore.Web package.

All the auto-generated API endpoints will use the POST HTTP method.

Annotating commands

For Eventuous to understand what commands need to be exposed as API endpoints and on what routes, those commands need to be annotated by the HttpCommand attribute:

[HttpCommand(Route = "payment", Aggregate = typeof(Booking))]
public record ProcessPayment(string BookingId, float PaidAmount);

You can skip the Route property, in that case Eventuous will use the command class name. For the example above the generated route would be processPayment. We recommend specifying the route explicitly as you might refactor the command class and give it a different name, and it will break your API if the route is auto-generated.

If your application has a single application service working with a single aggregate type, you don’t need to specify the aggregate type, and then use a different command registration method (described below).

Another way to specify the aggregate type for a group of commands is to annotate the parent class (command container):

[AggregateCommands(typeof(Booking))]
public static class BookingCommands {
    [HttpCommand(Route = "payment")]
    public record ProcessPayment(string BookingId, float PaidAmount);
}

In such case, Eventuous will treat all the commands defined inside the BookingCommands static class as commands operating on the Booking aggregate.

Also, you don’t need to specify the aggregate type in the command annotation if you use the MapAggregateCommands registration (see below).

Finally, you don’t need to annotate the command at all if you use the explicit command registration with the route parameter.

Registering commands

There are several extensions for IEndpointRouteBuilder that allow you to register HTTP endpoints for one or more commands.

Single command

The simplest way to register a single command is to make it explicitly in the bootstrap code:

var builder = WebApplication.CreateBuilder();

// Register the app service
builder.Services.AddApplicationService<BookingService, Booking>();

var app = builder.Build();

// Map the command to an API endpoint
app.MapCommand<ProcessPayment, Booking>("payment");

app.Run();

record ProcessPayment(string BookingId, float PaidAmount);

If you annotate the command with the HttpCommand attribute, and specify the route, you can avoid providing the route when registering the command:

app.MapCommand<BookingCommand, Booking>();
...

[HttpCommand(Route = "payment")]
public record ProcessPayment(string BookingId, float PaidAmount);

Multiple commands for an aggregate

You can also register multiple commands for the same aggregate type, without a need to provide the aggregate type in the command annotation. To do that, use the extension that will create an ApplicationServiceRouteBuilder, then register commands using that builder:

app
    .MapAggregateCommands<Booking>()
    .MapCommand<ProcessPayment>()
    .MapCommand<ApplyDiscount>("discount");
    
...

// route specified in the annotation
[HttpCommand(Route = "payment")] 
public record ProcessPayment(string BookingId, float PaidAmount);

// No annotation needed
public record ApplyDiscount(string BookingId, float Discount);

Discover commands

There are two extensions that are able to scan your application for annotated commands, and register them automatically.

First, the MapDiscoveredCommand<TAggregate>, which assumes your application only serves commands for a single aggregate type:

app.MapDiscoveredCommands<Booking>();

...
[HttpCommand(Route = "payment")] 
record ProcessPayment(string BookingId, float PaidAmount);

For it to work, all the commands must be annotated and have the route defined in the annotation.

The second extension will discover all the annotated commands, which need to have an association with the aggregate type by using the Aggregate argument of the attribute, or by using the AggregateCommands attribute on the container class (described above):

app.MapDiscoveredCommands();

...

[HttpCommand(Route = "bookings/payment", Aggregate = typeof(Booking))] 
record ProcessPayment(string BookingId, float PaidAmount);

[AggregateCommands(typeof(Payment))]
class V1.PaymentCommands {
    [HttpCommand(Route = "payments/register")]
    public record RegisterPayment(string PaymentId, string Provider, float Amount);
    
    [HttpCommand(Route = "payments/refund")]
    public record RefundPayment(string PaymentId);
}

Both extensions will scan the current assembly by default, but you can also provide a list of assemblies to scan as an argument:

app.MapDiscoveredCommands(typeof(V1.PaymentCommands).Assembly);

Using HttpContext data

Commands processed by the application service might include properties that aren’t provided by the API client, but are available in the HttpContext object. For example, you can think about the user that is making the request. The details about the user, and the user claims, are available in HttpContext.

You can instruct Eventuous to enrich the command before it gets sent to the application service, using the HttpContext data. In that case, you also might want to hide the command property from being exposed to the client in the OpenAPI spec.

To hide a property from being exposed to the client, use the JsonIgnore attribute:

[HttpCommand(Route = "book")]
public record BookRoom(string RoomId, string BookingId, [property: JsonIgnore] string UserId);

Then, you can use the HttpContext data in your command:

app
    .MapAggregateCommands<Booking>()
    .MapCommand<BookRoom>((cmd, ctx) => cmd with { UserId = ctx.User.Identity.Name });

When the command is mapped to the API endpoint like that, and the property is ignored, the OpenAPI specification won’t include the ignored property, and the application service will get the command populated with the user id from HttpContext.