Introducing Postie

What is Postie

Postie is a new library for ASP.NET Core that makes the Command Query Responsibility Segregation (CQRS) pattern a first-class citizen in your app. It combines a mediator pattern implementation designed specifically for CQRS with helper extensions to map your ASP.NET Core Minimal APIs directly to command and query handlers.

CQRS in Thirty Seconds

A quick refresher before we get going. CQRS (Command Query Responsibility Segregation) splits an application's operations into commands, which change things, and queries, which read things. Each one is a small, plain class, and each has a single handler that does the actual work. A dispatcher (usually called a mediator) sits in the middle, so the code that receives a request doesn't need to know anything about the handler.

The benefits: handlers are small and easy to test, cross-cutting concerns like validation and logging can be added once as pipeline behaviours, and your read models don't have to look anything like your write models. If you already live and breathe this stuff, skip ahead.

A command and a query flowing from HTTP requests through Postie's endpoint mapping and IEndpointDispatcher to your mediator, pipeline behaviours, and handlers, with the response shaped by Postie

The Itch

If you cleave to the idea, as I do, that your API handler should be as small as possible, handing off the work to a dispatcher, or even a good-old service implementation, then your code ends up looking very similar. You inject, call and craft an HTTP result:

app.MapPost("/orders", async (CreateOrder command, ISender sender, CancellationToken token) =>
{
    var order = await sender.Send(command, token);
    return Results.CreatedAtRoute("GetOrder", new { id = order.Id }, order);
});

It's only a few lines each time, but multiply that across a whole API and it adds up to a lot of copy-and-paste and a surprising number of small mistakes. Postie gets rid of this layer.

How Postie Works

It's pretty simple. Create your commands and queries using Postie.Cqrs or MediatR, and then hook them up with the Postie minimal API extensions:

var orders = app.MapGroup("/orders");
orders.MapQuery<GetOrders, IReadOnlyList<Order>>("/");
orders.MapQuery<GetOrder, Order?>("/{id:int}").WithName("GetOrder");
orders.MapQuery<SearchOrders, IReadOnlyList<Order>>("/search", QueryMethod.Post);
orders.MapPostCreate<CreateOrder, Order>("/", "GetOrder", o => new { id = o.Id });
orders.MapPutCommand<UpdateOrder, Order>("/{id:int}", binding: RequestBinding.Parameters);
orders.MapDeleteCommand<DeleteOrder>("/{id:int}");

No handler lambdas, no plumbing. Each mapping applies the conventions you would have written yourself: queries return 200, or 404 if the handler returns null; creates return 201 with a Location header built from a named route; commands with nothing to return give you a 204. Hybrid endpoints can take an id from the route and the rest from the body. Queries aren't limited to GET, either. If your search criteria are too rich for a query string, a query can bind from the request body over POST, or over the new HTTP QUERY method if you're feeling adventurous.

Example Query

Queries and handlers are also simple to create. In this example we're injecting an asynchronous IQueryable to represent our data, but it could be anything, like an EF DbContext.

public record GetOrder(int Id) : IQuery<Order?>;

internal class GetOrderHandler(IQueryable<Order> orders) : IQueryHandler<GetOrder, Order?>
{
    public async ValueTask<Order?> Handle(GetOrder query, CancellationToken ct) =>
        await orders.SingleOrDefaultAsync(o => o.Id == query.Id, ct);
}

Bring Your Own Mediator

The endpoint engine talks to a mediator through one small interface, IEndpointDispatcher, which has two methods. Postie ships with a lightweight mediator of its own, and an adapter for MediatR that leaves your existing IRequest types alone. If you use something else, implementing the interface yourself takes about a dozen lines.

The adapter targets MediatR 12.x, the last Apache-2.0 line, so the default setup doesn't cost anything. And if you'd rather move off MediatR altogether, the in-box mediator gives you separate command and query dispatchers, pipeline behaviours, streaming queries and OpenTelemetry tracing.

Honest by Design

One rule I settled on early: Postie only advertises what it produces. Each mapping declares the OpenAPI responses its own code can actually return, and nothing else. If you add validation and your endpoint can now return a 400, you chain .ProducesValidationProblem() on yourself. The mappings are just RouteHandlerBuilders, so all the usual composition works.

Configuration mistakes fail at startup rather than at run time. Map a body-bound command with [FromRoute] attributes on its members (a mistake that quietly binds the wrong data in most stacks) and Postie throws when the app starts, with a message that names the members to fix.

Eating the Dog Food

This library grew out of my own personal projects. I like to play with new patterns and development techniques and have the luxury of infinite dev time to spend on refactoring if I want. When I like a pattern I keep it and am always looking for ways to make it easy and remove boilerplate. I've been using a version of Postie for a few years now in my personal finance app, MooBank. Postie grew out of this code. MooBank has now been migrated onto Postie proper, as has my other main web app in active development, Wrangler CI.

Free Forever

Postie is MIT licensed and free, and it will stay that way. There won't be a commercial edition, licence keys, or a revenue threshold. I'm releasing this to promote use of a genuinely good pattern that's a good fit for ASP.NET Core.

Getting Started

Postie.Cqrs.AspNetCore is the easiest place to start; it brings in the endpoint mapping and the in-box mediator. Use Postie.AspNetCore.MediatR if you're staying on MediatR, and add Postie.AspNetCore.FluentValidation if you want validation failures returned as RFC 9457 problem details.

dotnet add package Postie.Cqrs.AspNetCore

The GitHub repository has the documentation, and the samples include two runnable Orders APIs, one on the in-box mediator and one on MediatR. Postie also has a permanent project page on this site. If you try it and something feels awkward, tell me in an issue.

What's Next

I have a few ideas on what to do next. Server-Sent Events for stream queries support will map a stream query to an SSE endpoint, pushing each result to the client as it's produced instead of waiting for the whole set. The use I have in mind is live progress for long-running jobs, like imports.

Following on from that, and in keeping with the CQRS theme, is some sugar for Entity Framework Core to enable injecting untracked IQueryables of your EF entities directly into your query handlers. A much cleaner approach than DbContext injection or some other data layer-specific object. You can see how it would work in my GetOrder example above.

Lastly, one of my custom endpoints that was deliberately excluded from Postie was an extension for producing a paged result. There are so many different ways to do this, it didn't feel in the spirit of a generic library to include mine. However, there's no harm in throwing it into its own library.

Releasing one of my personal libraries for the world to use is a new thing for me, and I'm excited to see where it goes. Give Postie a try, and let me know how you get on.