Non-Durable Transport Usage

NuGet Package:
NServiceBus.Transport.NonDurable 1.x
Target Version:
NServiceBus 10.x

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.

EndpointInline ExecutionHandlers
SagaEndpointEnabledOrderSaga, PaymentCompletedHandler
PaymentEndpointDisabledProcessPaymentHandler

What the sample demonstrates

  • Shared broker — Both endpoints use the same NonDurableBroker so 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

  1. Run the console application.
  2. Press Enter to send a PlaceOrder message.
  3. Observe the console output as the saga starts, sends ProcessPayment to PaymentEndpoint, and handles the PaymentCompleted event 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.

Related Articles

  • Non-durable persistence
    Non-durable persistence (previously known as In-Memory persistence) stores data in a non-durable manner.
  • Non-Durable Transport
    A transport for exchanging NServiceBus messages in memory in a non-durable fashion.