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¶
- Command Handling with MediatR and Command Handling with MassTransit cover the write side.
- Persisting Projections covers the read side.