# Non-durable persistence 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](/persistence/index.md#persistence-at-a-glance). |Feature | | |:--- |--- |Storage Types |Sagas, Outbox, Subscriptions |Transactions |Synchronized storage session |Concurrency control |Optimistic concurrency |Scripted deployment |Does not apply |Installers |Does not apply ## Configuration Configure the endpoint to use non-durable persistence: ```cs // Use NonDurable persistence for all concerns endpointConfiguration.UsePersistence(); // or select specific concerns endpointConfiguration.UsePersistence(); endpointConfiguration.UsePersistence(); endpointConfiguration.UsePersistence(); ``` Alternatively, use the non-durable-specific shorthand: ```cs endpointConfiguration.UseNonDurablePersistence(); ``` > [!NOTE] > The `UseNonDurablePersistence()` shorthand is unique to this persister. Other persistences use only the standard `UsePersistence()` pattern. ## Advanced configuration For scenarios that require additional control, use `UseNonDurablePersistence(NonDurablePersistenceOptions)`: ```cs 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: ```cs 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](/nservicebus/hosting/index.md), register `NonDurableStorage` in the service collection. The persister automatically resolves it from dependency injection before falling back to the options or default storage: ```cs services.AddSingleton(); ``` ### 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: ```cs 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](/nservicebus/sagas/saga-finding.md) via `ISagaFinder`. ### 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: ```cs class OrderSagaFinder : ISagaFinder { public async Task FindBy(CompleteOrder message, ISynchronizedStorageSession session, IReadOnlyContextBag context, CancellationToken cancellationToken = default) { var nonDurableSession = session.NonDurablePersistenceSession(); var sagaData = await nonDurableSession.FindSagaData( context, data => data.OrderId == message.OrderId, cancellationToken); return sagaData; } } class OrderSagaFinderWithState : ISagaFinder { public async Task FindBy(CompleteOrder message, ISynchronizedStorageSession session, IReadOnlyContextBag context, CancellationToken cancellationToken = default) { var nonDurableSession = session.NonDurablePersistenceSession(); var sagaData = await nonDurableSession.FindSagaData( context, message.OrderId, (data, orderId) => data.OrderId == orderId, cancellationToken); return 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: ```cs class TaskIndex { public ConcurrentDictionary ServerTaskIdToSagaId { get; } = new(); } class TaskSagaFinder(TaskIndex index, ISagaPersister persister) : ISagaFinder { public async Task 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(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 name | Description | |:---|:---| | `NServiceBus.NonDurable.Persistence.Saga.GetById` | Loading a saga by its identifier | | `NServiceBus.NonDurable.Persistence.Saga.GetByProperty` | Loading a saga by a correlated property | | `NServiceBus.NonDurable.Persistence.Saga.Save` | Saving a new saga instance | | `NServiceBus.NonDurable.Persistence.Saga.Update` | Updating an existing saga instance | | `NServiceBus.NonDurable.Persistence.Saga.Complete` | Completing a saga instance | ### Outbox spans | Span name | Description | |:---|:---| | `NServiceBus.NonDurable.Persistence.Outbox.BeginTransaction` | Beginning an outbox transaction | | `NServiceBus.NonDurable.Persistence.Outbox.Get` | Retrieving an outbox record | | `NServiceBus.NonDurable.Persistence.Outbox.Store` | Storing transport operations in the outbox | | `NServiceBus.NonDurable.Persistence.Outbox.SetAsDispatched` | Marking an outbox record as dispatched | ### Subscription spans | Span name | Description | |:---|:---| | `NServiceBus.NonDurable.Persistence.Subscription.Subscribe` | Subscribing to a message type | | `NServiceBus.NonDurable.Persistence.Subscription.Unsubscribe` | Unsubscribing from a message type | | `NServiceBus.NonDurable.Persistence.Subscription.GetSubscribers` | Resolving subscribers for a message type | See the [OpenTelemetry documentation](/nservicebus/operations/opentelemetry.md) 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](/nservicebus/sagas/concurrency.md)_ explains how these conflicts are handled, and contains guidance for high-load scenarios. By default, non-durable persistence uses [optimistic concurrency control](https://en.wikipedia.org/wiki/Optimistic_concurrency_control) when updating or deleting saga data. If a conflict is detected a `InvalidOperationException` will be thrown describing the conflict. ### Pessimistic concurrency Version 3.3.0 and above offer the option for pessimistic concurrency control in the saga persister. To configure pessimistic mode, configure the concurrency mode in the `NonDurableSagaOptions`. The pessimistic lock timeout (defaults to 30 seconds) can also be adjusted if necessary. ```cs var options = new NonDurablePersistenceOptions { Saga = new NonDurableSagaOptions { ConcurrencyMode = NonDurableSagaConcurrencyMode.Pessimistic, PessimisticLockTimeout = TimeSpan.FromSeconds(30) } }; endpointConfiguration.UseNonDurablePersistence(options); ``` When one message handler holds a lock on a saga data instance, another message handler will wait asynchronously until the lock is freed or until the `PessimisticLockTimeout` is reached, at which point a `NonDurableSagaLockTimeoutException` will be thrown and the message will be eligible for retry. Even when pessimistic locking is enabled, the optimistic concurrency version will still be checked before updated data is committed. Pessimistic locks occur only on data reads and do not affect new saga creation, where the optimistic concurrency check still prevents duplicate saga instances from being created. The saga locking mode is selected when a saga is created and persists for the life of that saga instance.