# Cosmos DB NoSQL

This guide provides step-by-step instructions for integrating Azure Cosmos DB NoSQL into your existing .NET application using the SAIF platform templates.

[TOC]

## 📚 Useful Resources

- [Azure Cosmos DB Documentation](https://docs.microsoft.com/en-us/azure/cosmos-db/)

- [Entity Framework Core Cosmos DB Provider](https://docs.microsoft.com/en-us/ef/core/providers/cosmos/)

- [Aspire Azure Cosmos DB Entity Framework Integration](https://learn.microsoft.com/en-us/dotnet/aspire/database/azure-cosmos-db-entity-framework-integration)

- [Cosmos DB Best Practices](https://docs.microsoft.com/en-us/azure/cosmos-db/best-practice-dotnet)

- [Azure Cosmos DB Visual Studio Code Extension](https://learn.microsoft.com/en-us/azure/cosmos-db/visual-studio-code-extension?tabs=CBDMongovCore&pivots=api-nosql)

## 📋 Prerequisites

Before you begin, ensure you have:

- ✅ An existing Azure API project created with SAIF platform templates

- ✅ SAIF platform templates installed

- ✅ Docker installed (for running Cosmos DB emulator locally)

## ⚡ Adding Cosmos DB NoSQL Feature

To add Cosmos DB NoSQL support to your existing application, run the following command in your project root directory:

```bash
dotnet new saif-feature-database-cosmosdb --cosmosdb_api NoSQL --name <your-app-name> --force --project_id <your-project-id>
```

### 🔧 Parameters

- **`--name`**: Replace `<your-app-name>` with your application name (e.g., "myapp")

- **`--project_id`**: Replace `<your-project-id>` with your project identifier (e.g., "it-api-exp-myapp")

- **`--cosmosdb_api`**: Set to "NoSQL" for Cosmos DB NoSQL API

- **`--force`**: Overwrites existing files if they exist

### 💡 Example

```bash
dotnet new saif-feature-database-cosmosdb --cosmosdb_api NoSQL --name myapp --force --project_id it-api-exp-myapp
```

This command will generate the following files and configurations:

### 📁 New Projects Added

- **Data Project**: Contains Entity Framework context and data models

- **Data.Seed Project**: Contains data seeding functionality

### 📄 Files Added to `infra/api/` folder

- `database-cosmodb-containers.yaml`: Container configuration

- `feature-database-cosmodb-vars.yaml`: Feature variables

- `feature-database-cosmosdb.tf`: Terraform file for parsing YAML configurations

### ➕ Additional Files

- `CosmosDbResourceBuilder.g.cs`: Extension method for Aspire integration

## ⚙️ Configuration Files

After running the template command, you'll find several new configuration files:

### 1. 📦 Container Configuration (`database-cosmodb-containers.yaml`)

```yaml
containers:
  - container_name: People
    partition_key: /PartitionKey
```

This file defines the Cosmos DB containers that will be created. You can add more containers as needed.

### 2. 🏷️ Feature Variables (`feature-database-cosmodb-vars.yaml`)

```yaml
api_type: nosql
```

This file specifies the Cosmos DB API type.

### 3. 🏗️ Terraform Infrastructure (`feature-database-cosmosdb.tf`)

This file contains the Terraform configuration for creating Cosmos DB resources and parsing the YAML configuration files.

## 👨‍💻 Developer Instructions

### 📦 Container Configuration

Configure your Cosmos DB containers in the `database-cosmodb-containers.yaml` file:

```yaml
containers:
  - container_name: People
    partition_key: /PartitionKey
  - container_name: Animals
    partition_key: /PartitionKey
  - container_name: PayrollReportDrafts
    partition_key: /PartitionKey
    unique_keys:
      - ["/userId", "/auditNumber"]   # composite: pair must be unique per partition
```

### 🔌 Adding to API

In your API project, add Cosmos DB services using the builder extension method:

```csharp
// In Program.cs of your API project
builder.Services.AddCosmosDbServices();
```

This extension method will:

- ✅ Register the DbContext with dependency injection

- ✅ Configure the Cosmos DB connection

- ✅ Set up Entity Framework services

### 🌟 Adding to Aspire

In your Aspire AppHost project, configure Cosmos DB and containers:

```csharp
// In Program.cs of your AppHost project
var (cosmosdb, database) = builder
    .AddCosmosDb(backend);

database
    .AddContainer("People", "/PartitionKey");
```

You can add multiple containers by chaining additional `AddContainer` calls:

```csharp
var (cosmosdb, database) = builder
    .AddCosmosDb(backend);

database
    .AddContainer("People", "/PartitionKey")
    .AddContainer("Animals", "/PartitionKey");
```

### 🏗️ Terraform Configuration

The template updates your `api.generated.tf` file to include Cosmos DB feature flags as parameters passed to the `saif-appservices` module:

```terraform
module "saif-appservices" {
  source               = "app.terraform.io/SAIFCorp/saif-apiservice/azure"

  # ... other module parameters ...

  FeatureFlags = {
    cosmosdb_nosql_serverless          = true
    cosmosdb_nosql_serverless_settings = local.feature_database_cosmosdb_nosql_serverless_settings
  }
}
```

#### ⏱️ Per-Container TTL

Each container supports an optional `ttl_in_days` field that controls Cosmos DB's [Time-to-Live](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/time-to-live) feature. Omitting the field leaves TTL fully disabled on the container.

```terraform
module "saif-appservices" {
  source  = "app.terraform.io/SAIFCorp/saif-apiservice/azure"

  # ... other module parameters ...

  feature_flags = {
    cosmosdb_nosql_serverless = true
    cosmosdb_nosql_serverless_settings = {
      containers = {
        orders   = { partition_key_path = "/id", ttl_in_days = 30 }    # documents expire after 30 days
        sessions = { partition_key_path = "/userId", ttl_in_days = 7 } # documents expire after 7 days
        archive  = { partition_key_path = "/id", ttl_in_days = -1 }    # TTL enabled; documents retained indefinitely unless item-level TTL is set
        events   = { partition_key_path = "/eventId" }                  # TTL disabled — no expiration, no TTL feature overhead
      }
    }
  }
}
```

| Value                 | TTL feature on container | Behaviour                                                              |
| --------------------- | ------------------------ | ---------------------------------------------------------------------- |
| omitted (default)     | Disabled                 | Documents are retained indefinitely; TTL is not evaluated by Cosmos DB |
| `-1`                  | Enabled                  | Documents are retained indefinitely unless an item-level TTL is set    |
| Positive whole number | Enabled                  | Documents expire after the specified number of days                    |

> ℹ️ The seconds conversion (`days × 86400`) is handled automatically inside the module. You never need to calculate or supply seconds directly.

> ⚠️ **Existing deployments**: If your container currently omits `ttl_in_days`, TTL remains disabled. Explicitly setting `ttl_in_days = -1` enables the TTL feature on the container (a plan-time Terraform change), though no documents will expire unless they carry an item-level TTL attribute.

#### 🔑 Unique Key Constraints

Each container supports an optional `unique_keys` field for defining [Cosmos DB unique key constraints](https://learn.microsoft.com/en-us/azure/cosmos-db/unique-keys). The value is a list of constraints — each constraint is itself a list of one or more paths. A single-path constraint enforces uniqueness on one field; a multi-path (composite) constraint enforces uniqueness across the combination of fields.

In the YAML container configuration (`database-cosmodb-containers.yaml`):

```yaml
containers:
  - container_name: Users
    partition_key: /PartitionKey
    unique_keys:
      - ["/email"]                        # single-field unique key
  - container_name: Reports
    partition_key: /PartitionKey
    unique_keys:
      - ["/userId", "/auditNumber"]       # composite unique key
```

> ⚠️ **Unique key constraints are immutable.** Cosmos DB does not allow modifying unique keys after a container is created. Adding a unique key to an existing container requires recreating (force-replacing) the container, which destroys existing data. Plan unique key requirements before your first deployment.

> ℹ️ **Existing projects:** If your project was created before `unique_keys` support was added, your `infra/app/feature-database-cosmosdb.tf` will not extract the field. Add `unique_keys = coalesce(try(container["unique_keys"], null), [])` to the container map transform:
>
> ```hcl
> feature_database_cosmosdb_containers_map = {
>   for container in local.feature_database_cosmosdb_containers :
>   container["container_name"] => {
>     partition_key_path = container["partition_key"]
>     unique_keys        = coalesce(try(container["unique_keys"], null), [])
>   }
> }
> ```

## 🗃️ Entity Framework Context Setup

The template generates a DbContext class for Cosmos DB. Here's what it typically looks like:

```csharp
using Microsoft.EntityFrameworkCore;

namespace YourApp.Data;

public class YourAppContext : DbContext
{
    public YourAppContext(DbContextOptions<YourAppContext> options)
        : base(options)
    {
    }

    public DbSet<Person> People { get; set; }
    public DbSet<Animal> Animals { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Person>(entity =>
        {
            entity.Property(p => p.Id);
            entity.ToContainer(nameof(People))
                  .HasPartitionKey(p => p.PartitionKey);
        });

        modelBuilder.Entity<Animal>(entity =>
        {
            entity.Property(a => a.Id);
            entity.ToContainer(nameof(Animals))
                  .HasPartitionKey(a => a.PartitionKey);
        });

        base.OnModelCreating(modelBuilder);
    }
}
```

## 📝 Creating Data Models

Create your data models by inheriting from `BaseEntity`:

```csharp
using YourApp.Data;

namespace YourApp.Models;

public class Person : BaseEntity
{
    public string FirstName { get; set; } = string.Empty;
    public string LastName { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
    public DateTime DateOfBirth { get; set; }
}

public class Animal : BaseEntity
{
    public string Name { get; set; } = string.Empty;
    public string Species { get; set; } = string.Empty;
    public string Breed { get; set; } = string.Empty;
    public int Age { get; set; }
}
```

The `BaseEntity` class provides common properties like `Id` and `PartitionKey`.

## 🔍 Self-Service Read-Only Access

You can grant Entra ID groups or user principals read-only access to your Cosmos DB data in **non-production environments** (test, qa, uat). This enables your team to browse application data through the Azure Portal Data Explorer without requiring manual portal steps or platform team intervention. Alternatively, you can authenticate to cosmos.azure.com and use the Data Explorer there.

### Configuring Data Readers

Configure `data_readers` in your `feature-database-cosmodb-vars.yaml` file as a map keyed by environment short name. Each environment maps to a list of reader entries. Each entry requires a `name` (descriptive label) and either an `object_id` or `group_name`:

```yaml
api_type: nosql

data_readers:
  test:
    - name: "Claims Dev Team"
      object_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  qa:
    - name: "Claims Dev Team"
      object_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
    - name: "QA Testers"
      group_name: "Claims-QA-Readers"
  uat:
    - name: "QA Testers"
      group_name: "Claims-QA-Readers"
```

The template automatically filters the list based on `var.environment_short_name` at plan time — only readers for the current environment are passed to the module.

Wire the local into your module call in `app.generated.tf`:

```terraform
module "saif-appservices" {
  source = "app.terraform.io/SAIFCorp/saif-apiservice/azure"
  # ...
  feature_flags = {
    cosmosdb_nosql_serverless              = true
    cosmosdb_nosql_serverless_settings     = local.feature_database_cosmosdb_nosql_serverless_settings
    cosmosdb_nosql_serverless_data_readers = local.feature_database_cosmosdb_data_readers
  }
}
```

| Field        | Description                                                                                                      | Required     |
| ------------ | ---------------------------------------------------------------------------------------------------------------- | ------------ |
| `name`       | Descriptive label for the reader entry (used as the Terraform state key — must be unique within the environment) | Yes          |
| `object_id`  | Entra ID object ID of a group or user principal                                                                  | One of these |
| `group_name` | Display name of an Entra ID security group (looked up automatically — see constraints below)                     | is required  |

> ℹ️ **Existing projects:** If your project was created before data reader support was added, you need to make three manual additions:
>
> **1.** Add the `data_readers` map to `feature-database-cosmodb-vars.yaml`:
> ```yaml
> data_readers:
>   test: []
>   qa: []
>   uat: []
> ```
>
> **2.** Add the `feature_database_cosmosdb_data_readers` local to `infra/api/feature-database-cosmosdb.tf` (use `infra/app/` for projects created before Forge 3.x):
> ```hcl
> feature_database_cosmosdb_data_readers = {
>   for r in try(local.feature_database_cosmosdb_vars_data["data_readers"][var.environment_short_name], []) :
>   r["name"] => {
>     object_id  = try(r["object_id"], null)
>     group_name = try(r["group_name"], null)
>   }
> }
> ```
>
> **3.** Add `cosmosdb_nosql_serverless_data_readers` to your module call in `app.generated.tf`:
> ```hcl
> cosmosdb_nosql_serverless_data_readers = local.feature_database_cosmosdb_data_readers
> ```

### Example: Team with Multiple Reader Groups

```yaml
api_type: nosql

data_readers:
  test:
    - name: "Development Team"
      group_name: "Claims-Dev-Team"
    - name: "Tech Lead"
      object_id: "abcdef01-2345-6789-abcd-ef0123456789"
  qa:
    - name: "Development Team"
      group_name: "Claims-Dev-Team"
    - name: "QA Team"
      group_name: "Claims-QA-Team"
  uat:
    - name: "QA Team"
      group_name: "Claims-QA-Team"
```

!!! warning "Group Membership"
    You are responsible for managing membership of the Entra ID groups you reference. Forge provisions the role assignment — it does not manage who belongs to the group.

!!! warning "`group_name` Constraints"
    Group lookup uses Entra ID display name with `security_enabled = true`. Two constraints apply:

    - **Display names must be unique** across all security groups in the tenant. If multiple security groups share the same display name, the Terraform plan will fail.
    - **M365 groups are not supported** — only security groups are matched.

    Prefer `object_id` when possible to avoid ambiguous lookups. Find the object ID in **Microsoft Entra ID** → **Groups** → select your group → copy the **Object ID** from the overview page.

### Accessing Cosmos DB Data

!!! warning "VPN Access Does Not Work"
    Cosmos DB accounts are deployed with private endpoints behind the corporate network. Connecting over VPN from home does **not** resolve to a SAIF IP address, so the Azure Portal Data Explorer and cosmos.azure.com will fail to connect. You must access Cosmos DB from a virtual machine inside the corporate network or be on the corporate network itself.

To browse and query your Cosmos DB data, use the [Azure Cosmos DB extension for VS Code](https://learn.microsoft.com/en-us/azure/cosmos-db/visual-studio-code-extension?tabs=CBDMongovCore&pivots=api-nosql) from within VS Code on a corporate VM:

1. Connect to a corporate VM (e.g. via SAIF VM)
2. Install the **Azure Cosmos DB** extension from the VS Code marketplace
3. Sign in with your Entra ID account
4. Browse your Cosmos DB accounts, databases, and containers directly in the sidebar
5. Run queries, view documents, and inspect container settings without leaving the editor

> 💡 Using VS Code Remote (SSH or Tunnel) to connect to the corporate VM lets you keep your local editor experience while accessing Cosmos DB through the VM's network.

---

## 📚 Related Documentation

- [Events with Cosmos Foundry Example](../../../foundry/events-with-cosmos.md) - Working example of event-driven architecture with Cosmos DB
