Who does what
Relayer is not another broker abstraction. It owns the work that is the same on every broker and leaves the rest to the client you already trust.
Relayer
- Serialization
- System.Text.Json source generation by default, so Native AOT works.
- Message identity
- Stable names like orders.placed;v=1 on the wire, never CLR type names.
- Failure handling
- Classification, immediate retries, delayed redelivery, dead-letter copies with full history.
- Outbox and inbox
- Sends inside a transaction are enlisted automatically.
- Telemetry
- OpenTelemetry messaging conventions, with one ActivitySource and one Meter.
- Startup validation
- Routes, handlers and endpoint capabilities are checked before any consumer starts.
The native client
- Sending and receiving
- Dekaf, the Azure SDK, the AWS SDK and RabbitMQ.Client do the I/O.
- Settlement
- Complete, abandon, ack, commit. Relayer only decides which one.
- Ordering
- Partition keys, sessions and FIFO message groups, used as they are.
- Dead-letter queues
- Native DLQs where the broker has them, with error metadata attached.
- Configuration
- ProducerBuilder, ServiceBusClientOptions, ConnectionFactory, passed through unchanged.
- The raw message
- Always one call away from a handler, through the feature collection.
A message, a handler, a route
Registration is explicit. There is no assembly scanning: the source generator writes the registration code, and you can read it.
- Analyzers catch a missing message name or an unregistered handler at compile time.
- Startup fails with every configuration error listed together, before a consumer runs.
- An ordering key on an endpoint that cannot order messages is an error, not a silent no-op.
[Message("orders.placed", Version = 1)]
public sealed record OrderPlaced(string OrderId, string CustomerId);
public sealed class OrderPlacedHandler(AppDbContext db) : IHandler<OrderPlaced>
{
public async ValueTask HandleAsync(OrderPlaced message, MessageContext context,
CancellationToken cancellationToken)
{
db.Invoices.Add(Invoice.For(message));
await db.SaveChangesAsync(cancellationToken);
}
}
services.AddRelayer(relayer =>
{
relayer.AddMessageTypesFromThisAssembly();
relayer.AddTransport<BrokerOptions, RouteOptions, ConsumerOptions>("events", broker =>
{
broker.Send<OrderPlaced>("orders.placed",
route => route.OrderingKey(static m => m.CustomerId));
broker.Consume("orders.placed",
consumer => consumer.Handler<OrderPlacedHandler>().Concurrency(8));
});
});
await sender.SendAsync(new OrderPlaced("A-1001", "c-42"), cancellationToken);
Where it stands
Relayer is built in the open. This is what works on main today.
| Configuration, routes and startup validation | Implemented |
|---|---|
| Source-generated handlers, messages and JSON | Implemented |
| Batch dispatch and receive executor composition | Implemented core seam |
| Send pipeline and middleware | Implemented |
| Consume pipeline and receive SPI | Implemented |
| Failure classification, retries and dead-letter copies | Implemented |
| Declared topology and JSON export | Implemented |
| In-memory transport for tests and samples | Implemented |
| Kafka, Service Bus, SQS/SNS and RabbitMQ transports | In progress |
| Health checks with application-provided adapters | Implemented |
| Built-in health adapters | Planned |
| EF Core and PostgreSQL outbox and inbox | Planned |
| Core tracing, consume metrics and failure logs | Implemented |
| Native transport and outbox telemetry | Planned |