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 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:
// 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();
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):
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:
Storage resolution follows this precedence:
- A
NonDurableStorageresolved from dependency injection - The
Storageproperty set onNonDurablePersistenceOptions - 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. 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. 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. 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.
Querying saga data through the synchronized storage session
When correlation logic is too complex to express through the standard saga mapping API, use INonDurableStorageSession. 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. 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. activity source when an OpenTelemetry listener is configured.
Saga spans
| Span name | Description |
|---|---|
NServiceBus. | Loading a saga by its identifier |
NServiceBus. | Loading a saga by a correlated property |
NServiceBus. | Saving a new saga instance |
NServiceBus. | Updating an existing saga instance |
NServiceBus. | Completing a saga instance |
Outbox spans
| Span name | Description |
|---|---|
NServiceBus. | Beginning an outbox transaction |
NServiceBus. | Retrieving an outbox record |
NServiceBus. | Storing transport operations in the outbox |
NServiceBus. | Marking an outbox record as dispatched |
Subscription spans
| Span name | Description |
|---|---|
NServiceBus. | Subscribing to a message type |
NServiceBus. | Unsubscribing from a message type |
NServiceBus. | Resolving 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.