Skip to content

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.

📚 Useful Resources

📋 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:

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. This template adds:

  • --cosmosdb_api: Set to "NoSQL" for Cosmos DB NoSQL API

💡 Example

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 (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)

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)

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:

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:

// 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:

// 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:

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:

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 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 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 feature. Omitting the field leaves TTL fully disabled on the container.

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. 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):

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:

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:

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:

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.

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:

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 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) 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:

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):

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:

cosmosdb_nosql_serverless_data_readers = local.feature_database_cosmosdb_data_readers

Example: Team with Multiple Reader Groups

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"

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 for the lookup constraints and how to find an object ID.

Accessing Cosmos DB Data

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