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

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)

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

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

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

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:

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:

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

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.

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.