This sample demonstrates two NServiceBus endpoints running in the same process, both using the Non-Durable Transport and Non-Durable Persistence. They share a single NonDurableBroker instance so messages flow between them in memory.
| Endpoint | Inline Execution | Handlers |
|---|---|---|
| SagaEndpoint | Enabled | OrderSaga, PaymentCompletedHandler |
| PaymentEndpoint | Disabled | ProcessPaymentHandler |
What the sample demonstrates
- Shared broker — Both endpoints use the same
NonDurableBrokerso they communicate without external infrastructure. - Inline execution — SagaEndpoint enables inline execution. When the saga sends a message to its own queue via
SendLocal, the handler runs synchronously in the saga's thread. - Normal async messaging — Events published by PaymentEndpoint flow through the shared broker and are picked up by SagaEndpoint's pump.
- Transaction sharing — With both transport and persistence non-durable and
SendsAtomicWithReceive(the default), saga updates and outgoing messages commit atomically. - Multi-hosting — Two endpoints in one process using keyed dependency injection.
Prerequisites
- No external infrastructure required. The sample runs entirely in memory.
Running the sample
- Run the console application.
- Press Enter to send a
PlaceOrdermessage. - Observe the console output as the saga starts, sends
ProcessPaymentto PaymentEndpoint, and handles thePaymentCompletedevent or a timeout.
Code walk-through
Endpoint configuration
builder.Services.AddSingleton(new NonDurableBroker());
var sagaEndpoint = new EndpointConfiguration("Samples.NonDurable.Saga");
sagaEndpoint.UsePersistence<NonDurablePersistence>();
sagaEndpoint.UseSerialization<SystemJsonSerializer>();
sagaEndpoint.AssemblyScanner().Disable = true;
sagaEndpoint.AddSaga<OrderSaga>();
sagaEndpoint.AddHandler<PaymentCompletedHandler>();
var sagaTransport = new NonDurableTransport(new NonDurableTransportOptions
{
InlineExecution = new InlineExecutionOptions()
});
var sagaRouting = sagaEndpoint.UseTransport(sagaTransport);
sagaRouting.RouteToEndpoint(typeof(ProcessPayment), "Samples.NonDurable.Payment");
var paymentEndpoint = new EndpointConfiguration("Samples.NonDurable.Payment");
paymentEndpoint.UsePersistence<NonDurablePersistence>();
paymentEndpoint.UseSerialization<SystemJsonSerializer>();
paymentEndpoint.AssemblyScanner().Disable = true;
paymentEndpoint.AddHandler<ProcessPaymentHandler>();
var paymentTransport = new NonDurableTransport();
paymentEndpoint.UseTransport(paymentTransport);
Both endpoints disable assembly scanning and explicitly register their handlers and sagas, which is required for multi-endpoint hosting. Each endpoint has its own NonDurableTransport instance, but they share the same NonDurableBroker. SagaEndpoint enables inline execution; PaymentEndpoint does not.
The saga
public class OrderSaga(ILogger<OrderSaga> logger) :
Saga<OrderSagaData>,
IAmStartedByMessages<PlaceOrder>,
IHandleMessages<PaymentCompleted>,
IHandleTimeouts<CancelOrder>
{
protected override void ConfigureHowToFindSaga(SagaPropertyMapper<OrderSagaData> mapper)
{
mapper.MapSaga(sagaData => sagaData.OrderId)
.ToMessage<PlaceOrder>(message => message.OrderId)
.ToMessage<PaymentCompleted>(message => message.OrderId);
}
public async Task Handle(PlaceOrder message, IMessageHandlerContext context)
{
logger.LogInformation("PlaceOrder received with OrderId {MessageOrderId}", message.OrderId);
var processPayment = new ProcessPayment
{
OrderId = Data.OrderId
};
logger.LogInformation("Sending ProcessPayment to PaymentEndpoint");
await context.Send(processPayment);
var timeout = DateTimeOffset.UtcNow.AddSeconds(30);
logger.LogInformation("Requesting CancelOrder timeout in 30 seconds");
await RequestTimeout<CancelOrder>(context, timeout);
}
public Task Handle(PaymentCompleted message, IMessageHandlerContext context)
{
logger.LogInformation("PaymentCompleted received with OrderId {MessageOrderId}. Completing saga.", message.OrderId);
MarkAsComplete();
return Task.CompletedTask;
}
public Task Timeout(CancelOrder state, IMessageHandlerContext context)
{
logger.LogInformation("CancelOrder timeout fired for OrderId {DataOrderId}. Completing saga.", Data.OrderId);
MarkAsComplete();
return Task.CompletedTask;
}
}
OrderSaga is started by a PlaceOrder message. It sends a ProcessPayment command to PaymentEndpoint and requests a CancelOrder timeout. When the PaymentCompleted event arrives, the saga completes. If the timeout fires first, the saga completes anyway.
Expected output
When Enter is pressed, the console shows output similar to:
Sent PlaceOrder with OrderId <guid>
PlaceOrder received with OrderId <guid>
Sending ProcessPayment to PaymentEndpoint
Requesting CancelOrder timeout in 30 seconds
Processing payment for OrderId <guid>
PaymentCompletedHandler received event for OrderId <guid>
PaymentCompleted received with OrderId <guid>. Completing saga.
If the endpoint is stopped before the PaymentCompleted event arrives, the CancelOrder timeout will fire instead:
CancelOrder timeout fired for OrderId <guid>. Completing saga.