﻿# Cosmos DB Persistence Usage with Encryption


This sample shows how to use [Azure Cosmos DB client-side encryption](https://learn.microsoft.com/azure/cosmos-db/how-to-always-encrypted) 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](https://learn.microsoft.com/en-us/azure/cosmos-db/local-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

* Receives the `StartOrder` message and initiates 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.
1. Start the `Client` project.
1. Press <kbd>S</kbd> in the client window to start an order.
1. 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:

<!-- snippet: CosmosDBConfig -->

```cs
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();

endpointConfiguration.UseTransport(new LearningTransport());
endpointConfiguration.UseSerialization<SystemJsonSerializer>();
endpointConfiguration.EnableInstallers();

builder.Services.AddNServiceBusEndpoint(endpointConfiguration);
```

<!-- endsnippet -->

In the non-transactional mode, the saga id is used as a partition key, and, therefore, 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.

<!-- snippet: EncryptionPolicy -->

```cs
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)
};
```

<!-- endsnippet -->

The policy uses [encryption policy format version 2](https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-always-encrypted#choosing-a-client-encryption-policy), which allows all saga data properties to be encrypted. `OrderId` and `OrderDescription` use randomized encryption, while `id` and `PartitionKey` use [deterministic encryption](https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-always-encrypted#randomized-vs-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.

> [!NOTE]
> The server deletes and recreates the `Samples.CosmosDB.Encryption` database each time it starts so that the sample always uses the expected encryption key and policy. Starting the server removes all existing sample data.

## Order saga data

The data stored by the saga is defined in the `OrderSagaData.cs` file in the `Server` project:

<!-- snippet: sagadata -->

```cs
public class OrderSagaData :
    ContainSagaData
{
    public Guid OrderId { get; set; }
    public string OrderDescription { get; set; }
}
```

<!-- endsnippet -->

## Order saga

The handlers for this data are in the `OrderSaga.cs` file in the `Server` project:

<!-- snippet: thesaga -->

```cs
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 {OrderId}. Starting Saga", Data.OrderId);

        var shipOrder = new ShipOrder
        {
            OrderId = message.OrderId
        };

        logger.LogInformation("Order will complete in 5 seconds");
        CompleteOrder timeoutData = new()
        {
            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 {OrderId} completed", Data.OrderId);
        MarkAsComplete();
        OrderCompleted orderCompleted = new()
        {
            OrderId = Data.OrderId
        };
        return context.Publish(orderCompleted);
    }
}
```

<!-- endsnippet -->
