Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Eventuous/eventuous/llms.txt

Use this file to discover all available pages before exploring further.

Eventuous provides two complementary approaches for wiring command services to HTTP endpoints: a Minimal API style using extension methods on IEndpointRouteBuilder, and a controller style using the CommandHttpApiBase<TState> base class. Both patterns read the request body as the command, invoke the registered ICommandService<TState>, and translate the Result<TState> into the correct HTTP status code and problem-details payload. Install the NuGet package to get started:
Eventuous.Extensions.AspNetCore

Minimal API approach

MapCommand<TCommand, TState>()

The simplest registration reads the route from the [HttpCommand] attribute that you place on your command class. Eventuous resolves ICommandService<TState> from the DI container, handles the command, and returns 200 OK with Result<TState>.Ok on success.
// Annotate the command with its route
[HttpCommand(Route = "/bookings/book")]
public record BookRoom(string BookingId, string RoomId, string GuestId);

// In Program.cs / WebApplication startup
app.MapCommand<BookRoom, BookingState>();
If you omit Route, Eventuous derives the path from the command class name with the first letter lowercased, so BookRoom becomes /bookRoom.

Explicit route and enrichment

Pass the route as the first argument to override the attribute or provide one when the command class is not annotated. The optional enrichCommand delegate lets you copy values from HttpContext (e.g., authenticated user identity) onto the command before it is dispatched:
app.MapCommand<BookRoom, BookingState>(
    "/bookings/book",
    enrichCommand: (cmd, ctx) => cmd with { GuestId = ctx.User.Identity!.Name! }
);
The delegate signature is EnrichCommandFromHttpContext<TCommand>:
public delegate TCommand EnrichCommandFromHttpContext<TCommand>(
    TCommand    command,
    HttpContext httpContext
);

CommandServiceRouteBuilder<TState> — fluent mapping

When a single state type owns several commands you can use the fluent builder returned by MapCommands<TState>() to register them all in one chain:
app.MapCommands<BookingState>()
   .MapCommand<BookRoom>()
   .MapCommand<RecordPayment>()
   .MapCommand<CancelBooking>("/bookings/cancel");
Each .MapCommand<TCommand>() call reads the route from [HttpCommand] on the command class. You can override the route per-call, and each call also accepts an enrichCommand delegate and an optional Action<RouteHandlerBuilder> for further configuration (e.g., adding metadata for Swagger):
app.MapCommands<BookingState>()
   .MapCommand<BookRoom>(
       enrichCommand: (cmd, ctx) => cmd with { GuestId = ctx.User.Identity!.Name! },
       configure:     b => b.WithTags("Bookings")
   );

Contract-to-command mapping

When your HTTP contract differs from your domain command (e.g., the route carries part of the data), supply a ConvertAndEnrichCommand<TContract, TCommand> delegate:
app.MapCommands<BookingState>()
   .MapCommand<BookRoomRequest, BookRoom>(
       "/bookings/book",
       (req, ctx) => new BookRoom(req.BookingId, req.RoomId, ctx.User.Identity!.Name!)
   );

[HttpCommand] attribute reference

PropertyTypeDescription
RoutestringHTTP POST route. Defaults to the camel-cased class name when omitted.
StateTypeTypeAggregate state type. Required when used without MapCommands<TState>().
PolicyNamestringComma-separated authorization policy names applied with RequireAuthorization.
A generic version HttpCommandAttribute<TState> sets StateType at compile time:
[HttpCommand<BookingState>(Route = "/bookings/book")]
public record BookRoom(string BookingId, string RoomId, string GuestId);

Controller approach

CommandHttpApiBase<TState>

Derive from this base class to expose commands through MVC controllers. Inject ICommandService<TState> via the constructor (primary-constructor syntax is idiomatic) and call the protected Handle<TCommand> method from each action:
[Route("/booking")]
public class CommandApi(ICommandService<BookingState> service)
    : CommandHttpApiBase<BookingState>(service) {

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

    [HttpPost("recordPayment")]
    public Task<ActionResult<Result<BookingState>.Ok>> RecordPayment(
        [FromBody] RecordPayment    cmd,
        CancellationToken           cancellationToken
    ) => Handle(cmd, cancellationToken);
}
Handle<TCommand> forwards the command to the service and calls result.AsActionResult() to produce the correct HTTP response.

Custom result type — CommandHttpApiBase<TState, TResult>

When you need to shape the response body differently, inherit the two-type-parameter variant and override AsActionResult:
[Route("/custom/booking")]
public class CommandApiWithCustomResult(ICommandService<BookingState> service)
    : CommandHttpApiBase<BookingState, CustomBookingResult>(service) {

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

    protected override ActionResult AsActionResult(Result<BookingState> result)
        => result.Match(
            ok => new OkObjectResult(new CustomBookingResult {
                GuestId = ok.State.GuestId,
                RoomId  = ok.State.RoomId
            }),
            error => error.Exception switch {
                ValidationException => MapValidationError(error),
                _                   => base.AsActionResult(result)
            }
        );
}

Contract-to-command mapping in controllers

If the HTTP contract must be converted to a different domain command, pass a CommandMap<HttpContext> to the base class and call the two-type-parameter overload of Handle:
protected virtual async Task<ActionResult<TResult>> Handle<TContract, TCommand>(
    TContract         httpCommand,
    CancellationToken cancellationToken
)
The map is configured via CommandMap<HttpContext> and registered in DI before being injected into the controller.

Result mapping

Both approaches share the same mapping logic from Result<TState> to HTTP responses:
Exception typeHTTP status
(no exception — success)200 OK
DomainException400 Bad Request (ValidationProblemDetails)
AggregateNotFoundException404 Not Found
OptimisticConcurrencyException409 Conflict
(any other exception)500 Internal Server Error
Error responses use the application/problem+json content type with ProblemDetails or ValidationProblemDetails.

Build docs developers (and LLMs) love