Brokers and clients for .NET

3 min read
On trails:EDANETstep 3/7

A broker or event stream is the infrastructure that carries messages. A client library is the .NET package that talks to it. The choice of service decides the delivery model (see broker versus event streamBroker vs event streamA broker hands messages out and removes them once acknowledged, a stream keeps a log that consumers read at their own pace.Opens in a new tab), and the package decides how much of the plumbing is left for the application. The shop’s Orders, Payments, Inventory and Notifications services could use any of the five below.

The five

ServiceWhat it is.NET clientDead-lettering
Azure Service BusManaged broker: queues, and topics with subscriptions. Transactions, sessions for ordering, duplicate detection. For high-value business messages.Azure.Messaging.ServiceBusBuilt in, one per queue and per subscription
RabbitMQBroker with exchanges (direct, fanout, topic, headers) that route to queues. Classic and quorum queues delete a message once it is acknowledged. Streams are append-only logs that keep messages and can be replayed.RabbitMQ.Client (version 7 is async)Dead-letter exchanges, set by policy or queue argument
Apache KafkaDistributed log: partitioned topics whose events are kept for a configured time, read by consumer groups.Confluent.Kafka (a binding to librdkafka)None for ordinary consumers. The application publishes failed records to a dead-letter topic. Kafka Connect sink connectors have their own.
Azure Event HubsManaged event streaming for telemetry and ingestion, with a Kafka-compatible endpoint. Retention is time-based.Azure.Messaging.EventHubsNo
Azure Event GridManaged event routing over HTTP and MQTT, with push or pull delivery and CloudEvents 1.0. For reacting to discrete events.Azure.Messaging.EventGrid, Azure.Messaging.EventGrid.NamespacesYes
Table 1. Messaging services and their main .NET clients

Microsoft’s guidance for Azure splits them by purpose: Event Grid for reactive events, Event Hubs for streaming, Service Bus for transactional messaging. The same shop can use all three: Service Bus for orders, Event Hubs for site telemetry, Event Grid for reacting to an item shipping. For how queues, topics and partitions differ, see queue versus topicQueue vs topicA queue gives each message to one consumer, a topic gives every subscriber its own copy.Opens in a new tab and partitions, offsets and consumer groupsPartitions, offsets and consumer groupsHow a stream splits a topic into lanes, marks each reader's place, and shares the work among a group.Opens in a new tab.

A raw client in code

This is RabbitMQ.Client 7, whose methods are all async. It declares a durable queue, publishes OrderPlaced, and consumes it with a manual acknowledgement.

RabbitClient.csC#
var factory = new ConnectionFactory { HostName = "localhost" };await using var connection = await factory.CreateConnectionAsync();await using var channel = await connection.CreateChannelAsync(); await channel.QueueDeclareAsync("orders.placed", durable: true, exclusive: false, autoDelete: false); var body = JsonSerializer.SerializeToUtf8Bytes(new OrderPlaced(Guid.NewGuid(), 42.50m));await channel.BasicPublishAsync(exchange: "", routingKey: "orders.placed", body: body); var consumer = new AsyncEventingBasicConsumer(channel);consumer.ReceivedAsync += async (_, ea) =>{    var evt = JsonSerializer.Deserialize<OrderPlaced>(ea.Body.Span);    // ... handle the event ...    await channel.BasicAckAsync(ea.DeliveryTag, multiple: false);};await channel.BasicConsumeAsync("orders.placed", autoAck: false, consumer);

The queue is declared durable, so the queue itself survives a broker restart. With autoAck: false the consumer must call BasicAckAsync to say it is done, which is how a classic queue knows it can delete the message. Publishing to the default exchange with the queue name as the routing key is the simplest route. Real topologies use named exchanges (see routing and topologyRouting and topologyHow a messaging system decides where each message goes: exchange types, subscription filters and partition keys.Opens in a new tab).

What the client leaves to you

A raw client sends and receives bytes. Everything else is application code: serializing the event, retrying a failed handler with a delay, moving a poison message to a dead-letter queueDead-letter queuesWhere a message goes when it cannot be processed, so the rest of the traffic keeps moving.Opens in a new tab where the broker has none, declaring the exchanges and queues, writing the outboxOutbox and inboxWrite the event in the same transaction as the data, publish it from there, and deduplicate on the way in.Opens in a new tab, making the consumer idempotentIdempotent consumersA consumer that gives the same result whether a message arrives once or five times.Opens in a new tab, and propagating trace context. That list is the reason messaging frameworks exist, and the next note covers them.


Sources