﻿# Azure Table Persistence


Certain features of NServiceBus require persistence to permanently store data. Among them are subscription storage, sagas, and outbox. Various storage options are available including Azure Table and Azure Cosmos DB Table API.

Azure Table Persistence stores NServiceBus data in [Azure Table storage](https://azure.microsoft.com/en-us/services/storage/tables/) or [Azure Cosmos DB using the Table API](https://learn.microsoft.com/en-us/azure/cosmos-db/table-support/).

## 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                    |   |
|:---                       |---
|Supported storage types    |Sagas, Outbox, Subscriptions
|Transactions               |Using TransactionalBatch, [with caveats](transactions.md)
|Concurrency control        |Optimistic concurrency
|Scripted deployment        |Not supported
|Installers                 |Supported. Subscription, the default table or saga tables derived by convention when no default table is set are created at runtime, when enabled.

## Enable Azure Table Persistence

First add a reference to the assembly that contains the Azure Table persistence, which is done by adding a NuGet package reference to `NServiceBus.Persistence.AzureTable`.

<!-- snippet: PersistenceWithAzure -->

```cs
var persistence = endpointConfiguration.UsePersistence<AzureTablePersistence>();
persistence.ConnectionString("DefaultEndpointsProtocol=https;AccountName=[ACCOUNT];AccountKey=[KEY];");
```

<!-- endsnippet -->

### Token-credentials

Enables usage of Microsoft Entra ID authentication such as [managed identities for Azure resources](https://learn.microsoft.com/en-us/azure/storage/tables/authorize-access-azure-active-directory) instead of the shared secret in the connection string.

Use the corresponding [`TableServiceClient`](https://learn.microsoft.com/en-us/dotnet/api/azure.data.tables.tableserviceclient.-ctor?view=azure-dotnet#azure-data-tables-tableserviceclient-ctor(system-uri-azure-core-tokencredential-azure-data-tables-tableclientoptions)) constructor overload when [creating the client passed to the persistence](configuration.md).


## Provisioned throughput rate-limiting with Azure Cosmos DB

When using the provisioned throughput feature, it is possible for the CosmosDB service to rate-limit usage, resulting in "request rate too large" `RequestFailedException`s indicated by a [429 status code](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/troubleshoot-request-rate-too-large).

> [!WARNING]
> When using the Azure Table persistence with the outbox enabled, "request rate too large" errors may result in handler re-execution and/or duplicate message dispatches depending on which operation is throttled.

> [!NOTE]
> Microsoft provides [guidance](https://learn.microsoft.com/en-us/azure/cosmos-db/monitor-request-unit-usage) on how to monitor request rate usage.

The Azure.Data.Tables SDK handles these exceptions by [automatically retrying the failed request](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/conceptual-resilient-sdk-applications#http-429) based on headers included in the response.
The retry policy can be adjusted through [`TableClientOptions`](https://learn.microsoft.com/en-us/dotnet/api/azure.data.tables.tableclientoptions?view=azure-dotnet) when initializing the `TableServiceClient`.



### 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.

#### Starting, updating or deleting saga data

Azure Table Persistence uses [optimistic concurrency control](https://en.wikipedia.org/wiki/Optimistic_concurrency_control) when updating or deleting saga data.

Example exception:

```
Azure.Data.Tables.TableTransactionFailedException: The update condition specified in the request was not satisfied.
RequestId:f0742973-2002-0060-09d3-f9afb2000000
Time:2022-11-16T15:53:36.0074148Z
Status: 412 (Precondition Failed)
ErrorCode: UpdateConditionNotSatisfied
```

### Supported saga properties' types

Azure Table Persistence supports the same set of types as [Azure Table Storage](https://learn.microsoft.com/en-us/rest/api/storageservices/understanding-the-table-service-data-model) and additional types that can be serialized into JSON using [Json.NET](https://www.newtonsoft.com/json). When a saga containing a property of an unsupported type is persisted, an exception containing the following information is thrown: `The property type 'the_property_name' is not supported on Azure Table Storage and it cannot be serialized with JSON.NET`.

#### Customization

Saga data serialization can be configured by providing a custom [JsonSerializerSettings](https://www.newtonsoft.com/json/help/html/T_Newtonsoft_Json_JsonSerializerSettings.htm) instance:

<!-- snippet: AzurePersistenceSagasJsonSerializerSettings -->

```cs
var persistence = endpointConfiguration.UsePersistence<AzureTablePersistence, StorageType.Sagas>();

persistence.JsonSettings(new JsonSerializerSettings
{
    Converters =
        {
            new IsoDateTimeConverter
            {
                DateTimeStyles = DateTimeStyles.RoundtripKind
            }
        }
});
```

<!-- endsnippet -->

or with a custom [JsonReader](https://www.newtonsoft.com/json/help/html/T_Newtonsoft_Json_JsonReader.htm) instance:

<!-- snippet: AzurePersistenceSagasReaderCreator -->

```cs
var persistence = endpointConfiguration.UsePersistence<AzureTablePersistence, StorageType.Sagas>();

persistence.ReaderCreator(
    readerCreator: textReader =>
    {
        return new JsonTextReader(textReader);
    });
```

<!-- endsnippet -->

or with a custom [JsonWriter](https://www.newtonsoft.com/json/help/html/T_Newtonsoft_Json_JsonWriter.htm) instance:

<!-- snippet: AzurePersistenceSagasWriterCreator -->

```cs
var persistence = endpointConfiguration.UsePersistence<AzureTablePersistence, StorageType.Sagas>();

persistence.WriterCreator(
    writerCreator: writer =>
    {
        return new JsonTextWriter(writer)
        {
            Formatting = Formatting.None
        };
    });
```

<!-- endsnippet -->

### Saga Correlation property restrictions

Saga correlation property values are subject to the underlying Azure Storage table `PartitionKey` and `RowKey` restrictions:

* Up to 1KB in size
* Cannot contain [invalid characters](https://learn.microsoft.com/en-us/rest/api/storageservices/Understanding-the-Table-Service-Data-Model#tables-entities-and-properties)

## Outbox

### Storage format

From version 7.0.1, the row key will include the endpoint name to distinguish it from other endpoints processing the same message.

> [!WARNING]
> In version 7.0.0, when the default partition key is not explicitly set, Outbox rows are not separated by endpoint name. As a result, multiple logical endpoints cannot share the same table since [message identities are not unique across endpoints from a processing perspective](/nservicebus/outbox/index.md#message-identity). To avoid conflicts, either assign each endpoint to a separate table or [override the partition key](transactions.md).
> 

> [!WARNING]
> Serialized transport operations for a single Outbox record cannot exceed the Azure Table 64 KiB string-property limit. Exceeding the limit causes processing to fail. If this limit affects the workload, [add a thumbs-up or share the use case on issue #815](https://github.com/Particular/NServiceBus.Persistence.AzureTable/issues/815).
