Cosmos DB SDK Basics
Complete the full lesson to earn 25 points — 50 with Pro
Work through each section, then tap “Mark as Complete” on the last one.
✦ Skip the page breaks, the wait, and see fewer ads — read each lesson on a single page with Pro
Lesson: Mastering the Azure Cosmos DB .NET SDK for NoSQL
Introduction: Why Cosmos DB Matters in Modern Architecture
In the landscape of modern application development, the ability to store and retrieve data at scale is a foundational requirement. Azure Cosmos DB is a globally distributed, multi-model database service designed to meet these needs, but its true power is unlocked through the official SDKs. When we talk about "Cosmos DB for NoSQL," we are referring to the core document-based API that allows developers to store JSON documents and query them with a syntax remarkably similar to SQL.
Why does mastering the SDK matter? Because a database is only as good as the client code that interacts with it. If you implement your data access layer incorrectly, you might suffer from high latency, unexpected costs, or poor application stability. The Cosmos DB SDK acts as the bridge between your application logic and the distributed storage engine. Understanding how to manage connections, handle partitioning, and optimize queries is not just a "nice to have" skill; it is essential for building applications that can handle millions of requests while keeping costs predictable.
This lesson serves as a deep dive into the .NET SDK for Cosmos DB. We will move past the basic "Hello World" examples and explore how to structure your code for production environments, how to handle the complexities of partitioning, and how to write efficient queries that don't break the bank. Whether you are building a small internal tool or a massive global platform, the principles covered here remain the same.
1. Setting the Foundation: The Client Lifecycle
One of the most critical aspects of working with the Cosmos DB SDK is how you handle the CosmosClient object. Many developers make the mistake of creating a new client instance every time they need to perform a database operation. This is a significant performance anti-pattern. The CosmosClient is designed to be a thread-safe singleton that manages connection pooling and internal state.
If you instantiate a new client for every request, your application will quickly exhaust the available socket connections on the host machine. This leads to SocketException errors and severe performance degradation. Instead, you should register the CosmosClient as a singleton in your Dependency Injection (DI) container.
Implementing Singleton Client Pattern
In a standard ASP.NET Core application, you can configure your service registration in Program.cs or Startup.cs as follows:
// Registering the CosmosClient as a Singleton
builder.Services.AddSingleton<CosmosClient>(serviceProvider =>
{
string connectionString = builder.Configuration["CosmosConnectionString"];
return new CosmosClient(connectionString, new CosmosClientOptions()
{
SerializerOptions = new CosmosSerializationOptions()
{
PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase
}
});
});
By using this approach, the SDK maintains a single pool of connections to the database, which is reused across all incoming web requests. This approach drastically reduces the overhead of establishing new TLS handshakes for every database call, significantly lowering your end-to-end latency.
Callout: The Singleton Pattern Explained The
CosmosClientis a heavy object. It maintains a connection pool, caches endpoint metadata, and manages background tasks like updating the account's consistency level and region availability. Reusing a single instance ensures that your application benefits from these pre-warmed connections and cached metadata, whereas creating new instances forces the SDK to perform these expensive setup operations repeatedly.
2. Understanding Containers and Partition Keys
If you have used traditional relational databases, you are likely accustomed to primary keys that uniquely identify a row. In Cosmos DB, the "Partition Key" is arguably more important than the ID itself. The partition key determines how your data is physically distributed across the underlying storage nodes.
When you create a container, you must specify a partition key path. For example, if you are storing user orders, you might choose /userId as your partition key. Every time you perform a "point read" (a lookup by ID), the SDK needs both the document ID and the partition key to find the data efficiently. If you provide the wrong partition key, the SDK has to perform a "fan-out" query, which searches every physical partition in the cluster, drastically increasing the Request Unit (RU) cost and latency.
Working with Partition Keys in Code
When you define your data models, you should always include the partition key property. Here is an example of a simple Order class:
public class Order
{
[JsonProperty("id")] // The unique identifier for the document
public string Id { get; set; }
[JsonProperty("userId")] // The partition key
public string UserId { get; set; }
public DateTime OrderDate { get; set; }
public decimal TotalAmount { get; set; }
}
When you perform an operation, you must pass the partition key explicitly:
// Performing a point read
ItemResponse<Order> response = await container.ReadItemAsync<Order>(
id: "order-123",
partitionKey: new PartitionKey("user-456")
);
Note: Always choose a partition key that has high cardinality. A high-cardinality key is one that has a large number of distinct values (like
userIdororderId), which allows Cosmos DB to distribute your data evenly across many partitions. Avoid keys likestatus(e.g., "Pending", "Completed"), as this will result in "hot partitions" where one partition handles all the traffic while others remain idle.
3. CRUD Operations: Beyond the Basics
CRUD (Create, Read, Update, Delete) seems simple, but the SDK offers several nuances that can help you write cleaner and safer code. For instance, the UpsertItemAsync method is often preferred over CreateItemAsync in scenarios where you want to ensure the document exists without manually checking first.
Implementing Robust CRUD
Consider the following pattern for updating a document. Instead of just replacing the item, you should use the ETag (Entity Tag) to implement optimistic concurrency control. This ensures that you do not accidentally overwrite changes made by another process between the time you read the document and the time you save it.
public async Task UpdateOrder(Order updatedOrder)
{
// Read the current item to get the ETag
ItemResponse<Order> response = await container.ReadItemAsync<Order>(
updatedOrder.Id,
new PartitionKey(updatedOrder.UserId)
);
Order existingOrder = response.Resource;
// Perform the update with an AccessCondition based on the ETag
ItemResponse<Order> updateResponse = await container.ReplaceItemAsync(
updatedOrder,
updatedOrder.Id,
new PartitionKey(updatedOrder.UserId),
new ItemRequestOptions { IfMatchEtag = existingOrder.ETag }
);
}
This pattern is essential in distributed systems. Without the ETag check, two concurrent threads could read the same document, modify different fields, and save their changes, with the last save overwriting the previous one entirely.
4. Querying Data with SQL
Cosmos DB allows you to query your data using a SQL-like syntax. While it looks like SQL, it is important to remember that this is a query engine built for JSON documents. You can query deeply nested properties, arrays, and even perform joins, though joins in NoSQL work differently than they do in relational databases.
Writing Efficient Queries
When writing queries, always keep the partition key in mind. A query that includes the partition key filter is a "scoped query," meaning it only targets one physical partition. A query that omits the partition key is a "cross-partition query," which is significantly more expensive.
// A scoped query example
string sql = "SELECT * FROM c WHERE c.userId = @userId AND c.totalAmount > @minAmount";
QueryDefinition query = new QueryDefinition(sql)
.WithParameter("@userId", "user-456")
.WithParameter("@minAmount", 100);
using FeedIterator<Order> iterator = container.GetItemQueryIterator<Order>(query);
while (iterator.HasMoreResults)
{
FeedResponse<Order> response = await iterator.ReadNextAsync();
foreach (var order in response)
{
Console.WriteLine($"Order ID: {order.Id}");
}
}
Tips for Query Performance
- Avoid
SELECT *: Explicitly select only the fields you need. If you only need theorderDate, don't pull the entire document, which might include a large list of line items. - Use Indexing Policies: By default, Cosmos DB indexes every path in your document. You can customize the indexing policy to exclude paths you never query, which reduces the RU cost of write operations.
- Filter by Partition Key: Whenever possible, include the partition key in your
WHEREclause.
Callout: Cross-Partition Queries A cross-partition query is not necessarily "bad," but it should be used with caution. If your container has hundreds of partitions, a cross-partition query must aggregate results from all of them, which consumes more resources and takes longer. Only use cross-partition queries when you truly need to search across the entire dataset.
5. Handling Change Feed
The Change Feed is one of the most powerful features of Cosmos DB. It provides a persistent, ordered record of all changes (inserts and updates) to documents in your container. This is perfect for event-driven architectures where you need to trigger downstream actions—like sending a confirmation email after an order is placed or updating an analytics dashboard.
You can consume the Change Feed using the ChangeFeedProcessor. This utility automatically manages the state (checkpoints) of your processing, so if your application restarts, it knows exactly where it left off.
Setting Up a Change Feed Processor
Container leaseContainer = database.GetContainer("leases");
Container sourceContainer = database.GetContainer("orders");
ChangeFeedProcessor processor = sourceContainer
.GetChangeFeedProcessorBuilder<Order>("ordersProcessor", HandleChangesAsync)
.WithInstanceName("worker-node-1")
.WithLeaseContainer(leaseContainer)
.Build();
await processor.StartAsync();
static async Task HandleChangesAsync(IReadOnlyCollection<Order> changes, CancellationToken ct)
{
foreach (var order in changes)
{
// Process the changed document
Console.WriteLine($"Order processed: {order.Id}");
}
}
The leaseContainer is crucial here. It stores the state of the processing, ensuring that each document is processed exactly once by your application, even if you have multiple instances of your application running in parallel.
6. Best Practices and Industry Standards
To build production-ready applications, you must move beyond the basic implementation and adopt industry-standard practices.
Request Unit (RU) Management
Every operation in Cosmos DB costs "Request Units." Understanding how to monitor these costs is vital. Every ItemResponse object you receive from the SDK contains a RequestCharge property. Log this value. If you see spikes in RU usage, it is a direct signal that your queries or data access patterns need optimization.
Error Handling and Retries
The Cosmos DB SDK has built-in retry logic for transient errors (like network blips or temporary rate limiting). However, you should still implement your own robust error handling for permanent failures, such as 404 Not Found or 403 Forbidden.
- Handle 429 Errors: A
429 Too Many Requestserror means you have exceeded your provisioned RU limit. While the SDK handles retries automatically, you should monitor how often these occur. If they are frequent, consider scaling up your container or optimizing your queries. - Use
System.Text.Json: The latest versions of the SDK are highly optimized forSystem.Text.Json. Avoid using older libraries if possible, as they can introduce performance overhead during serialization.
The Comparison Table: SDK Versions and Configuration
| Feature | Legacy SDK (v2) | Modern SDK (v3) |
|---|---|---|
| Performance | Slower, blocking | Faster, fully asynchronous |
| DI Support | Limited | Native, first-class support |
| API Surface | Complex, many classes | Streamlined, intuitive |
| Maintenance | Deprecated/End-of-life | Actively developed |
Warning: Never store sensitive information (like connection strings) in your source code. Always use Azure Key Vault or Environment Variables to inject the connection string at runtime. Using hardcoded credentials is a common cause of security breaches in cloud applications.
7. Common Pitfalls and How to Avoid Them
Even experienced developers encounter common issues when working with Cosmos DB. Being aware of these will save you hours of debugging.
The "N+1" Query Problem
Just like in relational databases, the N+1 problem occurs when you fetch a list of items and then execute a separate query for each item to fetch related data. In Cosmos DB, this is disastrous because each query consumes RUs. Always aim to retrieve all necessary data in a single query or use denormalization to keep related data in the same document.
Misunderstanding Consistency Levels
Cosmos DB offers five consistency levels, ranging from "Strong" to "Eventual." Many developers default to "Strong" because it sounds the most reliable. However, "Strong" consistency comes with the highest latency and cost. Most applications perform perfectly well with "Session" consistency, which provides read-your-writes guarantees within a client session while maintaining high performance. Only use "Strong" if your business logic explicitly requires strict linearizability across the entire globe.
Forgetting to Dispose of Resources
While the CosmosClient is a singleton, other objects like FeedIterator or Stream objects should be handled with care. Always use using statements for iterators to ensure that network connections are returned to the pool promptly.
8. Summary and Key Takeaways
Mastering the Cosmos DB SDK is about more than just calling methods; it is about understanding how your code interacts with a distributed storage system. By following these principles, you ensure your applications are performant, cost-effective, and resilient.
Key Takeaways
- Singleton Pattern is Mandatory: Always register your
CosmosClientas a singleton to manage connection pooling efficiently and prevent socket exhaustion. - Partitioning is Primary: Treat the partition key as a first-class citizen in your data model. Every CRUD operation and query should leverage the partition key to avoid expensive cross-partition scans.
- Optimize for RUs: Monitor the
RequestChargeproperty on every response. Use this data to refine your indexing policies and optimize your SQL queries. - Embrace Asynchrony: The SDK is built for asynchronous operations. Avoid blocking calls (
.Resultor.Wait()) at all costs, as they can lead to thread pool starvation in web applications. - Use Change Feed for Events: Leverage the Change Feed for building reactive architectures rather than polling the database for updates.
- Optimistic Concurrency: Always use
ETagfor document updates to prevent lost updates in highly concurrent environments. - Choose the Right Consistency: Match your consistency level to your application's actual needs rather than defaulting to the highest level, which is often unnecessary and expensive.
Common Questions (FAQ)
Q: Can I change my partition key after the container is created? A: No, the partition key is immutable once the container is created. You must choose it carefully during the design phase. If you absolutely must change it, you will need to create a new container and migrate your data.
Q: How do I handle large datasets that exceed the RU limit? A: You can implement "autoscale" on your container, which allows the throughput to scale automatically based on usage. Alternatively, review your queries to ensure they are not doing full-table scans.
Q: Is it better to have many small containers or one large container? A: Generally, it is better to have fewer, larger containers unless you have specific requirements for different throughput settings or security boundaries for different data sets. Managing many small containers increases the operational overhead and can lead to fragmented resource usage.
By applying these lessons, you are well on your way to building sophisticated, cloud-native applications that effectively harness the scale and speed of Azure Cosmos DB. Remember that the SDK is a tool—how you wield it defines the success of your data architecture.
Reach the last section to complete this lesson and earn points — you're on section 1 of 9.
- Azure Container Registry Basics
- Azure Container Registry Basics Quiz5q
- Build and Store Container Images
- Build and Store Container Images Quiz5q
- ACR Tasks for Building Images
- ACR Tasks for Building Images Quiz5q
- Deploy to Azure App Service
- Deploy to Azure App Service Quiz5q
- Environment Variables and Secrets
- Environment Variables and Secrets Quiz5q
- Azure Container Apps Overview
- Azure Container Apps Overview Quiz5q
- Environment and Revision Management
- Environment and Revision Management Quiz5q
- KEDA Event-Driven Scaling
- KEDA Event-Driven Scaling Quiz5q
- Azure Kubernetes Service Basics
- Azure Kubernetes Service Basics Quiz5q
- AKS Manifest Files
- AKS Manifest Files Quiz5q
- Container Monitoring and Troubleshooting
- Container Monitoring and Troubleshooting Quiz5q
- Cosmos DB SDK Basics
- Cosmos DB SDK Basics Quiz5q
- Query Optimization
- Query Optimization Quiz5q
- Indexing Policies
- Indexing Policies Quiz5q
- Consistency Levels
- Consistency Levels Quiz5q
- Vector Similarity Search in Cosmos DB
- Vector Similarity Search in Cosmos DB Quiz5q
- Change Feed Processor
- Change Feed Processor Quiz5q
- PostgreSQL SDK Basics
- PostgreSQL SDK Basics Quiz5q
- Schema Design and Data Types
- Schema Design and Data Types Quiz5q
- PostgreSQL Indexing Strategies
- PostgreSQL Indexing Strategies Quiz5q
- pgvector for Vector Workloads
- pgvector for Vector Workloads Quiz5q
- Vector Similarity Search in PostgreSQL
- Vector Similarity Search in PostgreSQL Quiz5q
- RAG Patterns with PostgreSQL
- RAG Patterns with PostgreSQL Quiz5q
- OpenTelemetry SDK Basics
- OpenTelemetry SDK Basics Quiz5q
- Distributed Tracing
- Distributed Tracing Quiz5q
- KQL for Log Analytics
- KQL for Log Analytics Quiz5q
- Metrics Analysis
- Metrics Analysis Quiz5q
- Application Insights Integration
- Application Insights Integration Quiz5q
- Alerting and Diagnostics
- Alerting and Diagnostics Quiz5q
- Managed Identity Configuration
- Managed Identity Configuration Quiz5q
- Private Endpoints
- Private Endpoints Quiz5q
- Network Security Groups
- Network Security Groups Quiz5q
- Certificate Management
- Certificate Management Quiz5q
- RBAC for AI Services
- RBAC for AI Services Quiz5q
- Service Principal Authentication
- Service Principal Authentication Quiz5q
Enjoying the courses?
Everything stays free. Pro shows fewer ads, doubles the points you earn on every lesson and quiz so you progress twice as fast, unlocks half of every practice exam — plus full case studies — with the Learn & Exam study modes, and lets you read each lesson on one page.
- ✓ Fewer advertisements
- ✓ 2× points per lesson & quiz
- ✓ 50% of every exam unlocked
- ✓ Learn & Exam modes
- ✓ Distraction-free lessons