Getting Started
Architecture
NServiceBus
Transports
Persistence
Hosting
ServiceInsight
ServicePulse
ServiceControl
Monitoring
Modernization
Samples

Non-durable persistence

NuGet Package:
NServiceBus.Persistence.NonDurable 3.x
Target Version:
NServiceBus 10.x

Some scenarios require a non-durable persistence such as the development environment, testing, high-throughput scenarios where speed outweighs the benefits of durability, or a lightweight client not interested in durability across restarts:

Persistence at a glance

For a description of each feature, see the persistence at a glance legend.

Feature
Storage TypesSagas, Outbox, Subscriptions
TransactionsSynchronized storage session
Concurrency controlOptimistic concurrency
Scripted deploymentDoes not apply
InstallersDoes not apply

Configuration

Configure the endpoint to use non-durable persistence:

// Use NonDurable persistence for all concerns
endpointConfiguration.UsePersistence<NonDurablePersistence>();

// or select specific concerns
endpointConfiguration.UsePersistence<NonDurablePersistence, StorageType.Sagas>();
endpointConfiguration.UsePersistence<NonDurablePersistence, StorageType.Subscriptions>();
endpointConfiguration.UsePersistence<NonDurablePersistence, StorageType.Outbox>();

Alternatively, use the non-durable-specific shorthand:

endpointConfiguration.UseNonDurablePersistence();

Advanced configuration

For scenarios that require additional control, use UseNonDurablePersistence(NonDurablePersistenceOptions):

var sharedStorage = new NonDurableStorage();

var options = new NonDurablePersistenceOptions
{
    Storage = sharedStorage,
    TimeProvider = System.TimeProvider.System,
    Saga = new NonDurableSagaOptions
    {
        JsonSerializerOptions = new JsonSerializerOptions
        {
            TypeInfoResolverChain = { new SagaJsonContext() }
        }
    }
};

endpointConfiguration.UseNonDurablePersistence(options);

Shared storage

When multiple endpoints need to share the same in-memory state, provide a NonDurableStorage instance through the options:

var sharedStorage = new NonDurableStorage();

var options = new NonDurablePersistenceOptions
{
    Storage = sharedStorage
};

endpointConfiguration.UseNonDurablePersistence(options);

Storage resolution follows this precedence:

  1. A NonDurableStorage resolved from dependency injection
  2. The Storage property set on NonDurablePersistenceOptions
  3. A default shared storage instance

When using the generic host or multi-endpoint hosting, register NonDurableStorage in the service collection. The persister automatically resolves it from dependency injection before falling back to the options or default storage:

services.AddSingleton<NonDurableStorage>();

Time provider

Supply a custom System.TimeProvider to control how timestamps and outbox entry expiry are calculated. This is useful for testing scenarios that need deterministic time behavior.

Saga serialization

Saga data is the only persistence state that is JSON-serialized. By default, System.Text.Json is used with reflection. For AOT-compatible deployments or trimmed applications, provide a source-generated serializer context:

var options = new NonDurablePersistenceOptions
{
    Saga = new NonDurableSagaOptions
    {
        JsonSerializerOptions = new JsonSerializerOptions
        {
            TypeInfoResolverChain = { new SagaJsonContext() }
        }
    }
};

endpointConfiguration.UseNonDurablePersistence(options);

Transactions

Non-durable persistence participates in transactions through the synchronized storage session.

When a transport publishes a System.Transactions.Transaction into the transport transaction, the persister enlists as a volatile resource manager. Saga, outbox, and subscription operations are staged during the handler and committed or rolled back as a unit through the transaction.

When no transport transaction is available, the persister falls back to a standalone storage transaction that is committed when the handler completes successfully.

Custom saga finders

Non-durable persistence supports custom saga finders via ISagaFinder<TSagaData, TMessage>.

Querying saga data through the synchronized storage session

When correlation logic is too complex to express through the standard saga mapping API, use INonDurableStorageSession.GetSagaData to query the in-memory saga store directly from within a custom finder:

class OrderSagaFinder :
    ISagaFinder<OrderSagaData, CompleteOrder>
{
    public Task<OrderSagaData> FindBy(CompleteOrder message, ISynchronizedStorageSession session, IReadOnlyContextBag context, CancellationToken cancellationToken = default)
    {
        var nonDurableSession = session.NonDurablePersistenceSession();

        var sagaData = nonDurableSession.GetSagaData<OrderSagaData>(
            context,
            data => data.OrderId == message.OrderId,
            cancellationToken);

        return Task.FromResult(sagaData);
    }
}

class OrderSagaFinderWithState :
    ISagaFinder<OrderSagaData, CompleteOrder>
{
    public Task<OrderSagaData> FindBy(CompleteOrder message, ISynchronizedStorageSession session, IReadOnlyContextBag context, CancellationToken cancellationToken = default)
    {
        var nonDurableSession = session.NonDurablePersistenceSession();

        var sagaData = nonDurableSession.GetSagaData<OrderSagaData, string>(
            context,
            message.OrderId,
            (data, orderId) => data.OrderId == orderId,
            cancellationToken);

        return Task.FromResult(sagaData);
    }
}

class CompleteOrder : IMessage
{
    public string OrderId { get; set; } = string.Empty;
}

class OrderSagaData : ContainSagaData
{
    public string OrderId { get; set; } = string.Empty;
}

The query is evaluated against a moment-in-time snapshot of the underlying storage. Entries added or removed concurrently may or may not be included. The returned saga data is a copy of the stored entry, and optimistic concurrency checks still apply if the saga is later updated or completed.

For unit testing, use TestableNonDurableSynchronizedStorageSession to create a fake session backed by an in-memory store.

Using a custom index with ISagaPersister

When maintaining a custom lookup index outside of the persister, resolve the saga ID from the index and delegate to ISagaPersister.Get to load the saga data. This still captures the saga entry for optimistic concurrency checks:

class TaskIndex
{
    public ConcurrentDictionary<Guid, Guid> ServerTaskIdToSagaId { get; } = new();
}

class TaskSagaFinder(TaskIndex index, ISagaPersister persister)
    : ISagaFinder<TaskSagaData, ContinueTask>
{
    public async Task<TaskSagaData> FindBy(ContinueTask message,
        ISynchronizedStorageSession storageSession, IReadOnlyContextBag context,
        CancellationToken cancellationToken = default)
    {
        if (!index.ServerTaskIdToSagaId.TryGetValue(message.ServerTaskId, out var sagaId))
        {
            return null;
        }

        return await persister.Get<TaskSagaData>(sagaId, storageSession, (ContextBag)context, cancellationToken);
    }
}

class ContinueTask : IMessage
{
    public Guid ServerTaskId { get; set; }
}

class TaskSagaData : ContainSagaData
{
    public Guid ServerTaskId { get; set; }
}

OpenTelemetry instrumentation

Non-durable persistence emits spans via the NServiceBus.Persistence.NonDurable activity source when an OpenTelemetry listener is configured.

Saga spans

Span nameDescription
NServiceBus.NonDurable.Persistence.Saga.GetByIdLoading a saga by its identifier
NServiceBus.NonDurable.Persistence.Saga.GetByPropertyLoading a saga by a correlated property
NServiceBus.NonDurable.Persistence.Saga.SaveSaving a new saga instance
NServiceBus.NonDurable.Persistence.Saga.UpdateUpdating an existing saga instance
NServiceBus.NonDurable.Persistence.Saga.CompleteCompleting a saga instance

Outbox spans

Span nameDescription
NServiceBus.NonDurable.Persistence.Outbox.BeginTransactionBeginning an outbox transaction
NServiceBus.NonDurable.Persistence.Outbox.GetRetrieving an outbox record
NServiceBus.NonDurable.Persistence.Outbox.StoreStoring transport operations in the outbox
NServiceBus.NonDurable.Persistence.Outbox.SetAsDispatchedMarking an outbox record as dispatched

Subscription spans

Span nameDescription
NServiceBus.NonDurable.Persistence.Subscription.SubscribeSubscribing to a message type
NServiceBus.NonDurable.Persistence.Subscription.UnsubscribeUnsubscribing from a message type
NServiceBus.NonDurable.Persistence.Subscription.GetSubscribersResolving subscribers for a message type

See the OpenTelemetry documentation for instructions on how to enable tracing in an endpoint.

Saga concurrency

When simultaneously handling messages, conflicts may occur. See below for examples of the exceptions which are thrown. Saga concurrency explains how these conflicts are handled, and contains guidance for high-load scenarios.

Starting a saga

Example exception:

System.InvalidOperationException: The saga with the correlation id 'Name: OrderId Value: f05c6e0c-aea6-48d6-846c-d1663998ebf2' already exists

Updating or deleting saga data

Non-durable persistence uses optimistic concurrency control when updating or deleting saga data.

Example exception:

System.InvalidOperationException: Saga with Id '7ac53d15-4742-4e38-9e2f-6d75c25b6621' can't be updated because it was updated by another process.

Related Articles