Skip to content

Exposing an API

EventSourcingKit is transport-agnostic: it does not care whether requests arrive over REST, GraphQL, or anything else. You expose your application by wiring its two sides to an API of your choice: commands on the write side and projections on the read side.

The Two Sides

  • The write side accepts a command and dispatches it to a handler, which stores events. This is where MediatR or MassTransit comes in.
  • The read side answers queries from a projection, through the read interface you registered with AddProjection.

An API endpoint is therefore thin: it translates a request into a command or a query and returns the result.

REST with Controllers

A controller injects the dispatcher and the projection repository. A POST sends a command; a GET reads from the projection and maps it to a response model:

[ApiController]
[Route("/books")]
public class BookController(
    IMediator mediator,
    IProjectionRepository<Book, Guid> books
) : ControllerBase
{
    [HttpPost("register")]
    public Task<Guid> RegisterBook(
        [FromBody] RegisterBookCommand command,
        CancellationToken cancellationToken
    ) =>
        mediator.Send(command, cancellationToken);

    [HttpGet("{id}")]
    public async Task<BookResponse> GetBook(
        Guid id,
        CancellationToken cancellationToken
    )
    {
        var book = await books.GetById(id, cancellationToken);
        return book.ToResponse();
    }
}

GraphQL with HotChocolate

With HotChocolate, a mutation field dispatches a command and a query field reads a projection. Here the command is sent over MassTransit's request client, which shows how the same projection feeds either transport:

public class BookMutationType : ObjectTypeExtension<Mutation>
{
    protected override void Configure(
        IObjectTypeDescriptor<Mutation> descriptor
    ) =>
        descriptor
            .Field("registerBook")
            .Argument("input", a => a.Type<RegisterBookCommandType>())
            .Resolve<Guid>(RegisterBook);

    private static async Task<Guid> RegisterBook(IResolverContext context)
    {
        var command = context.ArgumentValue<RegisterBookCommand>("input");
        var client = context.Service<IRequestClient<RegisterBookCommand>>();

        var response = await client.GetResponse<RegisterBookCommandResult>(
            command,
            context.RequestAborted
        );

        return response.Message.BookId;
    }
}

A query field, on the query type, is even simpler, resolving straight from the projection repository:

descriptor
    .Field("books")
    .Resolve<IEnumerable<Book>>(context =>
        context
            .Service<IProjectionRepository<Book, Guid>>()
            .GetAll(context.RequestAborted)
    );

For More Information