Request/Reply
Request/Reply is the pattern for “I need an answer.” You send a message, another service handles it and replies, and your await returns the response when it arrives. Asynchronous over the network, synchronous in your code.
It is still point-to-point — one request, one responder — but unlike plain Send, the caller blocks on the reply rather than walking away.
When to use it
Section titled “When to use it”- You need a value back — a quote, a status, a lookup result.
- The caller cannot make progress without the response.
- One service owns the answer. Fan-out to multiple responders is Scatter-Gather, not this pattern.
Rule of thumb: if you’d reach for a synchronous HTTP call, you probably want request/reply. It carries the same shape — caller waits for callee — but runs over the message bus, which gives you retries, timeouts, and the same correlation-id story as the rest of your messaging.
The contract
Section titled “The contract”Two message types — the request and the reply:
using ServiceConnect.Interfaces;
public sealed class QuoteRequest(Guid correlationId) : Message(correlationId){ public string ProductCode { get; init; } = string.Empty;}
public sealed class QuoteResponse(Guid correlationId) : Message(correlationId){ public decimal Price { get; init; }}Both derive from Message. The correlation id on the reply carries through from the request — that’s how ServiceConnect matches an incoming reply to the pending await on the requester.
The requester
Section titled “The requester”Two things are different from plain SendAsync:
- The requester must be consuming. Replies land on the requester’s queue, so the bus has to be started before the request goes out.
- You call
SendRequestAsync<TRequest, TReply>andawaitthe reply.
using Microsoft.Extensions.DependencyInjection;using QuoteDemo.Contracts;using ServiceConnect;using ServiceConnect.Client.RabbitMQ;using ServiceConnect.Interfaces;using ServiceConnect.Interfaces.Options;
var services = new ServiceCollection();services.AddLogging();services.AddServiceConnect(builder =>{ builder.UseRabbitMQ(t => { t.Host = "localhost"; t.Username = "guest"; t.Password = "guest"; }); builder.ConfigureQueues(q => q.QueueName = "quotes-requester");});
await using var provider = services.BuildServiceProvider();var bus = provider.GetRequiredService<IBus>();
await bus.StartConsumingAsync(); // required: replies come back on our queue
var response = await bus.SendRequestAsync<QuoteRequest, QuoteResponse>( new QuoteRequest(Guid.NewGuid()) { ProductCode = "widget" }, new RequestOptions { EndPoint = "quotes-responder", Timeout = 30_000 });
Console.WriteLine($"Got price {response.Price}");RequestOptions.Timeout is in milliseconds and defaults to 10,000. When the timeout elapses without a reply, SendRequestAsync throws — a request without a bounded wait would leak state forever, so ServiceConnect does not let you opt out.
The responder
Section titled “The responder”The responder is a normal message handler with one extra rule: reply via IConsumeContext.ReplyAsync, not via a fresh SendAsync.
using QuoteDemo.Contracts;using ServiceConnect.Interfaces;
public sealed class QuoteRequestHandler : IMessageHandler<QuoteRequest>{ public async Task HandleAsync(QuoteRequest message, IConsumeContext context, CancellationToken cancellationToken = default) { var price = PriceFor(message.ProductCode); await context.ReplyAsync(new QuoteResponse(message.CorrelationId) { Price = price, }); }
private static decimal PriceFor(string code) => code switch { "widget" => 42.50m, _ => 0m, };}ReplyAsync sets a ResponseMessageId header that ServiceConnect uses to correlate the reply back to the caller’s pending await. A plain Bus.SendAsync back to the requester’s queue will not resolve the outstanding request — it would arrive, land in the queue, and go nowhere. Always use ReplyAsync when replying to a request.
The responder’s bootstrap is structurally identical to any other consumer — register the handler, name the queue, call StartConsumingAsync. See Handlers for the full shape.
Timeouts, not retries
Section titled “Timeouts, not retries”Request/reply has timeout semantics, not retry semantics. If the responder is slow or unreachable, the caller eventually sees a timeout exception; the request itself is not automatically re-sent. This is deliberate — a request is usually a user-facing operation where the caller wants to know quickly that something is wrong, rather than wait another 30 seconds for a retry.
If you need resilient at-least-once delivery with background retries, a plain SendAsync + an event-based reply path is the right shape, not a blocking request/reply.
The request methods raise RequestSendCancelledException (inheriting from OperationCanceledException) when the outbound send pipeline cancels before the request reaches the broker — distinct from a timeout (RequestTimeoutException) and from caller-token cancellation (OperationCanceledException). It applies to SendRequestAsync, SendRequestMultiAsync, and PublishRequestAsync.
Correlation-id flow
Section titled “Correlation-id flow”ServiceConnect wires the correlation id through for you:
- The request carries a correlation id you generate (or that your handler inherited from an upstream message).
- The responder’s reply uses
message.CorrelationId— the id from the request. - Any logging, tracing, or process-manager state tied to that id can now follow the conversation end-to-end.
If your responder needs to publish downstream events as side effects, pass message.CorrelationId through to those events too. That is how a web of related messages stays tied to a single user action.
Configured destinations
Section titled “Configured destinations”Like Send, request/reply can omit EndPoint if you have mapped the request type to a queue at startup:
builder.ConfigureQueues(q =>{ q.QueueName = "quotes-requester"; q.AddQueueMapping(typeof(QuoteRequest), "quotes-responder");});
// Later:var response = await bus.SendRequestAsync<QuoteRequest, QuoteResponse>( new QuoteRequest(Guid.NewGuid()) { ProductCode = "widget" }, new RequestOptions { Timeout = 30_000 });Inline RequestOptions.EndPoint still overrides the mapping per call. As with Send, if no mapping and no endpoint exists, the call fails — ServiceConnect does not guess.
Reference
Section titled “Reference”IBus.SendRequestAsync— single-reply requestIBus.SendRequestMultiAsync— multi-reply request- Message options —
RequestOptionsfor timeouts and expected replies
What comes next
Section titled “What comes next”- Scatter-Gather — request/reply with several responders and a bounded set of replies.
- Point-to-Point — for when you don’t need an answer.
- Handlers — the
Context.ReplyAsyncmethod in context.