---
title: Cosmos DB NoSQL
description: Add Cosmos DB NoSQL data access to a Forge application with platform templates.
moved_from:
  - guides/development/data/cosmos-nosql.md
---

# 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`, `--project_id`, and `--force` follow the [shared database feature template options](index.md#database-feature-templates). This template adds:

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

### 💡 Example

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

This command adds the [shared Data, Data.Seed, and Aspire resource builder projects](index.md#database-feature-templates) (`CosmosDbResourceBuilder.g.cs`) and these Cosmos DB files:

### 📄 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

## ⚙️ 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

When you select Cosmos DB as the database type on the API template (`--database_type cosmosdb`), `app.generated.tf` includes a conditional block that wires the Cosmos DB feature flags into the `saif-appservices` module call:

```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     = local.feature_database_cosmosdb_nosql_serverless_settings
    cosmosdb_nosql_serverless_data_readers = local.feature_database_cosmosdb_data_readers
  }
}
```

Without this block, `feature_flags` is never passed to the module, `ConnectionStrings__CosmosDbConnection` is never emitted, and the API fails at startup in `AddCosmosDbContext`.

> ℹ️ **Existing projects:** If your project was created before this wiring was added, add the `feature_flags` block above to the `module "saif-appservices"` call in `infra/api/app.generated.tf`. This applies to **Forge v3 projects only**: the block uses the v3 snake_case module interface, and mixing it into a Forge v2 project (`infra/app/app.generated.tf`, PascalCase `FeatureFlags`) produces an invalid mixed module call. If your project is still on Forge v2, [complete the v2 to v3 migration](../../learn/migration/forge-v2-to-v3.md) first. The block also assumes `local.feature_database_cosmosdb_data_readers` already exists; if your project predates data reader support, add that local first via **steps 1 and 2 only** of the existing-projects note under [Configuring Data Readers](#configuring-data-readers) below. Skip step 3 there, since the `feature_flags` block above already wires `cosmosdb_nosql_serverless_data_readers` for you, and adding it again would produce a duplicate attribute in the `feature_flags` object.

#### ⏱️ 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.

!!! note "Reader access is blocked in production"
    Reader access is **enforced as non-production only by the `cosmosdb` module** — even if a `prod` key appears in `data_readers`, no reader role assignments will be created when `is_production = true`.

### 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.

For a newly scaffolded project, `cosmosdb_nosql_serverless_data_readers` is already wired into the `feature_flags` block in `app.generated.tf` (see [Terraform Configuration](#terraform-configuration) above) — populating `data_readers` in the YAML file above is the only step you need. If your project predates data reader support, see the **Existing projects** note below instead of editing the module call directly.

| 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](../identity/configuration/entra-security-groups.md#group_name-constraints)) | 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 and `group_name` lookups"
    You manage membership of the groups you reference, and `group_name` matches only uniquely named Entra ID security groups. Prefer `object_id`; see [Entra Security Groups for Access Grants](../identity/configuration/entra-security-groups.md) for the lookup constraints and how to find an object ID.

### 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 Example](../examples/events-with-cosmos.md) - Working example of event-driven architecture with Cosmos DB
