Cosmos DB Persistence Usage with Encryption

NuGet Package:
NServiceBus.Persistence.CosmosDB 3.x
Target Version:
NServiceBus 9.x

This sample shows how to use Azure Cosmos DB client-side encryption to encrypt selected saga properties before they leave the process. The sample uses a client/server scenario in which the server stores saga data with Cosmos DB Persistence.

Prerequisites

Ensure that an instance of the latest Azure Cosmos DB Emulator is running.

Sample structure

This sample contains three projects, SharedMessages, Client and Server.

SharedMessages

The shared message contracts used by all endpoints.

Client

  • Sends the StartOrder message to Server.
  • Receives and handles the OrderCompleted event.

Server

  • Receive the StartOrder message and initiate an OrderSaga.
  • OrderSaga requests a timeout with an instance of CompleteOrder with the saga data.
  • OrderSaga publishes an OrderCompleted event when the CompleteOrder timeout fires.

Running the sample

  1. Start the Server project and wait for the endpoint to report that it has started.
  2. Start the Client project.
  3. Press S in the client window to start an order.
  4. After the saga timeout expires, verify that the server reports the completed saga and the client receives the OrderCompleted event.

Implementation highlights

Persistence config

In Program.cs of the Server project, the endpoint is configured to use Cosmos DB Persistence:

var endpointConfiguration = new EndpointConfiguration("Samples.CosmosDB.Encryption.Server");

var persistence = endpointConfiguration.UsePersistence<CosmosPersistence>();
persistence.DatabaseName("Samples.CosmosDB.Encryption");
// NServiceBus must use the encryption-enabled client so saga reads and writes apply the encryption policy.
persistence.CosmosClient(cosmosClient);
persistence.DefaultContainer("Server", "/id");
// The container is created explicitly with its encryption policy, so persistence must not create it.
persistence.DisableContainerCreation();

In the non-transactional mode, the saga id is used as a partition key, and thus, the container needs to use /id as the partition key path.

Encryption setup

The server wraps the regular CosmosClient with the Cosmos DB encryption client before passing it to NServiceBus. It then creates a client encryption key and a container with an encryption policy.

var containerProperties = new ContainerProperties(ContainerName, PartitionKeyPath)
{
    DefaultTimeToLive = -1,

    ClientEncryptionPolicy = new ClientEncryptionPolicy(
    [
        // NServiceBus derives the saga ID from the correlation value and performs a point read,
        // so the correlated property can use randomized encryption.
        Encrypted("/OrderId"),
        Encrypted("/OrderDescription"),

        // Policy format version 2 allows encrypting id and partition key
        // but requires deterministic encryption for these properties.
        EncryptedDeterministic("/id"),
        EncryptedDeterministic("/PartitionKey")

        // Deliberately NOT encrypted:
        //   /_NServiceBus-Persistence-Metadata  - pessimistic locking patches a path INSIDE
        //                                         this object (SagaDataContainer-ReservedUntil).
        //                                         Only top-level paths can be encrypted, so
        //                                         including it would encrypt the whole subtree
        //                                         and break the lock.
    ],
    policyFormatVersion: 2)
};

The policy uses encryption policy format version 2, which allows all saga data properties to be encrypted. OrderId and OrderDescription use randomized encryption, while id and PartitionKey use deterministic encryption because Cosmos DB requires these values for point reads and routing. The NServiceBus persistence metadata remains unencrypted because pessimistic locking updates a nested property within that object.

The key encryption key resolver stores a generated RSA private key under the server output directory. This is suitable only for demonstrating the encryption flow. Production systems should use a secure key store, such as Azure Key Vault, and must retain the key for as long as encrypted data needs to be read.

Order saga data

The data stored on the saga is defined in the OrderSagaData.cs file in the Server project:

public class OrderSagaData :
    ContainSagaData
{
    public Guid OrderId { get; set; }
    public string OrderDescription { get; set; }
}

Order saga

The handlers for this data are in the OrderSaga.cs file in the Server project:

public class OrderSaga(ILogger<OrderSaga> logger) :
    Saga<OrderSagaData>,
    IAmStartedByMessages<StartOrder>,
    IHandleTimeouts<CompleteOrder>
{
   
    protected override void ConfigureHowToFindSaga(SagaPropertyMapper<OrderSagaData> mapper)
    {
        mapper.MapSaga(saga => saga.OrderId).ToMessage<StartOrder>(msg => msg.OrderId);
    }

    public Task Handle(StartOrder message, IMessageHandlerContext context)
    {
        var orderDescription = $"The saga for order {message.OrderId}";
        Data.OrderDescription = orderDescription;
        logger.LogInformation($"Received StartOrder message {Data.OrderId}. Starting Saga");

        var shipOrder = new ShipOrder
        {
            OrderId = message.OrderId
        };

        logger.LogInformation("Order will complete in 5 seconds");
        var timeoutData = new CompleteOrder
        {
            OrderDescription = orderDescription,
        };

        return Task.WhenAll(
            context.SendLocal(shipOrder),
            RequestTimeout(context, TimeSpan.FromSeconds(5), timeoutData)
        );
    }

    public Task Timeout(CompleteOrder state, IMessageHandlerContext context)
    {
        logger.LogInformation($"Saga with OrderId {Data.OrderId} completed");
        MarkAsComplete();
        var orderCompleted = new OrderCompleted
        {
            OrderId = Data.OrderId
        };
        return context.Publish(orderCompleted);
    }
}

Related Articles

  • Sagas
    Master NServiceBus sagas to coordinate distributed workflows and ensure reliable long-running processes.