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)¶
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)¶
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:
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_flagsblock above to themodule "saif-appservices"call ininfra/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, PascalCaseFeatureFlags) produces an invalid mixed module call. If your project is still on Forge v2, complete the v2 to v3 migration first. The block also assumeslocal.feature_database_cosmosdb_data_readersalready 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 thefeature_flagsblock above already wirescosmosdb_nosql_serverless_data_readersfor you, and adding it again would produce a duplicate attribute in thefeature_flagsobject.
âąī¸ 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 settingttl_in_days = -1enables 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_keyssupport was added, yourinfra/app/feature-database-cosmosdb.tfwill not extract the field. Addunique_keys = coalesce(try(container["unique_keys"], null), [])to the container map transform:
đī¸ 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_readersmap tofeature-database-cosmodb-vars.yaml:2. Add the
feature_database_cosmosdb_data_readerslocal toinfra/api/feature-database-cosmosdb.tf(useinfra/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_readersto your module call inapp.generated.tf:
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:
- Connect to a corporate VM (e.g. via SAIF VM)
- Install the Azure Cosmos DB extension from the VS Code marketplace
- Sign in with your Entra ID account
- Browse your Cosmos DB accounts, databases, and containers directly in the sidebar
- 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 - Working example of event-driven architecture with Cosmos DB