Skip to main content

Azure Cosmos DB C# Tutorial: Complete Guide (2026)

Learn Azure Cosmos DB with C# in this step-by-step tutorial. Master partition keys, RUs, LINQ queries, and .NET SDK best practices. Start building today!

If you have ever watched a SQL Server database buckle under traffic from three continents at once, you already understand why Azure Cosmos DB exists. Working with Azure Cosmos DB C# applications gives you a globally distributed, multi-model database with single-digit millisecond reads, 99.999% availability, and elastic scale — all reachable through a first-class .NET SDK. This tutorial walks you through everything from your first container to advanced patterns like bulk ingestion, optimistic concurrency, and change feed processing.

We will use the Microsoft.Azure.Cosmos v3 SDK on .NET 8/9, and every example is runnable. More importantly, we will explain why each decision matters — because in Cosmos DB, an innocent-looking design choice can multiply your monthly bill by ten.

What Is Azure Cosmos DB and Why Should C# Developers Care?

Azure Cosmos DB is Microsoft's globally distributed, horizontally partitioned NoSQL database. Unlike a traditional relational database where you scale up by buying a bigger machine, Cosmos DB scales out by spreading your data across physical partitions and, optionally, across Azure regions worldwide.

Three characteristics define it:

  • Turnkey global distribution. Adding a read region in London, Sydney, or Toronto is a checkbox — no replication scripts, no failover tooling.
  • Guaranteed latency and throughput. Microsoft backs P99 latency of <10 ms for point reads with a financial SLA.
  • Tunable consistency. Five levels (Strong, Bounded Staleness, Session, Consistent Prefix, Eventual) let you trade consistency for latency and cost deliberately rather than accidentally.

The trade-off is that you pay for provisioned throughput measured in Request Units per second (RU/s), and you must choose a partition key up front. Get these two things right and Cosmos DB is superb. Get them wrong and you will be paying for a system that feels slower than a single SQL Server instance.

Cosmos DB APIs: Which One Do You Want?

Cosmos DB exposes several wire protocols: NoSQL (formerly SQL/Core), MongoDB, Cassandra, Gremlin, and Table. For greenfield .NET work, always pick the NoSQL API. It is the native API, it ships features first, and it has the richest C# SDK. The other APIs exist primarily as migration paths.

Setting Up the Azure Cosmos DB .NET SDK

Install the SDK from NuGet:

// dotnet add package Microsoft.Azure.Cosmos
// dotnet add package Azure.Identity

using Microsoft.Azure.Cosmos;
using Azure.Identity;

var options = new CosmosClientOptions
{
    // Direct mode uses TCP and is significantly faster than Gateway mode.
    ConnectionMode = ConnectionMode.Direct,
    // Serializer that matches modern .NET conventions.
    SerializerOptions = new CosmosSerializationOptions
    {
        PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase
    },
    // Route reads to the nearest region automatically.
    ApplicationPreferredRegions = new List<string>
    {
        Regions.EastUS, Regions.WestEurope, Regions.AustraliaEast
    },
    MaxRetryAttemptsOnRateLimitedRequests = 9,
    MaxRetryWaitTimeOnRateLimitedRequests = TimeSpan.FromSeconds(30)
};

// Prefer Entra ID (managed identity) over connection strings in production.
var client = new CosmosClient(
    accountEndpoint: "https://your-account.documents.azure.com:443/",
    tokenCredential: new DefaultAzureCredential(),
    clientOptions: options);

Critical pitfall: CosmosClient is thread-safe and expensive to construct — it caches routing tables, warms TCP connections, and maintains partition maps. Create one singleton per account for the lifetime of your application. Creating a client per request is the single most common cause of "why is Cosmos DB so slow?" support tickets.

// Program.cs — ASP.NET Core registration
builder.Services.AddSingleton(sp =>
{
    var config = sp.GetRequiredService<IConfiguration>();
    return new CosmosClient(
        config["Cosmos:Endpoint"],
        new DefaultAzureCredential(),
        new CosmosClientOptions { ConnectionMode = ConnectionMode.Direct });
});

Creating Databases and Containers in C#

A Cosmos DB account holds databases; databases hold containers; containers hold items (JSON documents). Here is idempotent bootstrap code you can safely run at startup:

Database database = await client.CreateDatabaseIfNotExistsAsync(
    id: "RetailStore",
    throughput: null); // null = throughput lives at the container level

var containerProperties = new ContainerProperties(
    id: "Orders",
    partitionKeyPath: "/customerId")
{
    DefaultTimeToLive = -1, // TTL enabled, but no automatic expiry unless set per item
    IndexingPolicy = new IndexingPolicy
    {
        IndexingMode = IndexingMode.Consistent,
        Automatic = true
    }
};

// Autoscale scales between 10% and 100% of max RU/s and is ideal for spiky workloads.
Container container = await database.CreateContainerIfNotExistsAsync(
    containerProperties,
    ThroughputProperties.CreateAutoscaleThroughput(maxAutoscaleThroughput: 4000));

Choosing a Partition Key: The Decision That Matters Most

The partition key determines how Cosmos DB distributes your data across physical partitions (each capped at 50 GB and 10,000 RU/s). You cannot change it without migrating the container, so think carefully.

A good partition key has three properties:

  • High cardinality — many distinct values, so data spreads evenly. /customerId or /tenantId is good; /country or /status is usually terrible.
  • Even request distribution — no single value receives a disproportionate share of traffic. This avoids the dreaded "hot partition."
  • Alignment with your most frequent query — queries that filter on the partition key are single-partition queries, which are dramatically cheaper than fan-out queries.

If no single field works, use a synthetic partition key by concatenating fields, or Cosmos DB's hierarchical partition keys (up to three levels), which let you query efficiently at any prefix level:

var hierarchical = new ContainerProperties(
    id: "Telemetry",
    partitionKeyPaths: new List<string> { "/tenantId", "/deviceId", "/date" });

await database.CreateContainerIfNotExistsAsync(hierarchical, throughput: 400);

// Query efficiently by tenant alone, or tenant + device.
var prefixKey = new PartitionKeyBuilder()
    .Add("tenant-42")
    .Add("device-7")
    .Build();

CRUD Operations with the Azure Cosmos DB C# SDK

Define a POCO. Cosmos DB requires an id property (string) and the partition key property must be present on every item.

public record Order
{
    public string Id { get; init; } = Guid.NewGuid().ToString();
    public string CustomerId { get; init; } = default!;
    public string Status { get; init; } = "Pending";
    public decimal Total { get; init; }
    public DateTimeOffset CreatedUtc { get; init; } = DateTimeOffset.UtcNow;
    public List<OrderLine> Lines { get; init; } = new();
}

public record OrderLine(string Sku, int Quantity, decimal UnitPrice);

Create, Read, Replace, Delete

var order = new Order
{
    CustomerId = "cust-1001",
    Total = 149.97m,
    Lines = { new OrderLine("KB-01", 3, 49.99m) }
};

var pk = new PartitionKey(order.CustomerId);

// CREATE — fails with 409 Conflict if the id already exists in this partition.
ItemResponse<Order> created = await container.CreateItemAsync(order, pk);
Console.WriteLine($"Create cost {created.RequestCharge} RU");

// POINT READ — the cheapest operation in Cosmos DB (~1 RU for a 1 KB item).
// Always prefer this over a query when you know id + partition key.
Order fetched = await container.ReadItemAsync<Order>(order.Id, pk);

// UPSERT — creates or replaces.
await container.UpsertItemAsync(fetched with { Status = "Shipped" }, pk);

// PATCH — sends only the delta; cheaper and avoids lost updates.
await container.PatchItemAsync<Order>(
    id: order.Id,
    partitionKey: pk,
    patchOperations: new[]
    {
        PatchOperation.Set("/status", "Delivered"),
        PatchOperation.Increment("/total", 5.00)
    });

// DELETE
await container.DeleteItemAsync<Order>(order.Id, pk);

Why point reads matter: a point read costs roughly 1 RU. The equivalent SELECT * FROM c WHERE c.id = 'x' query costs 2.5–3 RU even on a single partition. At a million reads a day, that difference is real money.

Handling Errors Correctly

try
{
    var response = await container.ReadItemAsync<Order>(id, pk);
    return response.Resource;
}
catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
    return null; // Expected case — do not log this as an error.
}
catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests)
{
    // 429 = rate limited. The SDK retries automatically; reaching here means
    // retries were exhausted. Scale up RU/s or add backpressure.
    logger.LogWarning("Throttled. Retry after {Delay}", ex.RetryAfter);
    throw;
}

Querying: SQL and LINQ in Azure Cosmos DB

Cosmos DB's NoSQL API speaks a SQL dialect over JSON. Use parameterised queries — string concatenation is both a security risk and a query-plan-cache killer.

var query = new QueryDefinition(
        "SELECT * FROM c WHERE c.customerId = @customerId AND c.total > @min ORDER BY c.createdUtc DESC")
    .WithParameter("@customerId", "cust-1001")
    .WithParameter("@min", 100);

var results = new List<Order>();
double totalRu = 0;

using FeedIterator<Order> iterator = container.GetItemQueryIterator<Order>(
    query,
    requestOptions: new QueryRequestOptions
    {
        PartitionKey = new PartitionKey("cust-1001"), // single-partition = cheap
        MaxItemCount = 100
    });

while (iterator.HasMoreResults)
{
    FeedResponse<Order> page = await iterator.ReadNextAsync();
    totalRu += page.RequestCharge;
    results.AddRange(page);
}

Console.WriteLine($"Returned {results.Count} orders for {totalRu:F2} RU");

Prefer LINQ when you want compile-time safety:

using Microsoft.Azure.Cosmos.Linq;

var linqFeed = container
    .GetItemLinqQueryable<Order>(
        requestOptions: new QueryRequestOptions { PartitionKey = pk })
    .Where(o => o.Status == "Shipped" && o.Total > 100)
    .OrderByDescending(o => o.CreatedUtc)
    .Take(20)
    .ToFeedIterator();

while (linqFeed.HasMoreResults)
{
    foreach (var o in await linqFeed.ReadNextAsync())
        Console.WriteLine($"{o.Id}: {o.Total:C}");
}

Never call .ToList() on a Cosmos LINQ queryable. Use ToFeedIterator(); otherwise the SDK cannot stream pages and you lose access to RequestCharge.

Continuation Tokens for Stateless Paging

public async Task<(List<Order> Items, string? Next)> GetPageAsync(
    string customerId, string? continuationToken, int pageSize = 25)
{
    using var iterator = container.GetItemQueryIterator<Order>(
        new QueryDefinition("SELECT * FROM c WHERE c.customerId = @id")
            .WithParameter("@id", customerId),
        continuationToken,
        new QueryRequestOptions
        {
            PartitionKey = new PartitionKey(customerId),
            MaxItemCount = pageSize
        });

    var page = await iterator.ReadNextAsync();
    return (page.ToList(), page.ContinuationToken);
}

Continuation tokens are the correct paging mechanism in Cosmos DB. There is no OFFSET-based paging that scales — OFFSET 10000 LIMIT 25 still reads and charges for all 10,025 documents.

Advanced Azure Cosmos DB C# Patterns

Optimistic Concurrency with ETags

Every Cosmos DB item carries an _etag that changes on every write. Use it to prevent lost updates:

ItemResponse<Order> current = await container.ReadItemAsync<Order>(id, pk);
var updated = current.Resource with { Status = "Cancelled" };

try
{
    await container.ReplaceItemAsync(updated, id, pk,
        new ItemRequestOptions { IfMatchEtag = current.ETag });
}
catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.PreconditionFailed)
{
    // Someone else wrote first — re-read and reapply your change.
}

Transactional Batch

Cosmos DB supports ACID transactions within a single logical partition:

TransactionalBatchResponse batch = await container
    .CreateTransactionalBatch(new PartitionKey("cust-1001"))
    .CreateItem(newOrder)
    .PatchItem(cartId, new[] { PatchOperation.Set("/checkedOut", true) })
    .ExecuteAsync();

if (!batch.IsSuccessStatusCode)
    throw new InvalidOperationException($"Batch failed: {batch.StatusCode}");

Bulk Ingestion

For large imports, enable bulk mode and fan out with Task.WhenAll. The SDK groups operations by partition automatically and can improve throughput by an order of magnitude:

var bulkClient = new CosmosClient(endpoint, credential,
    new CosmosClientOptions { AllowBulkExecution = true });

var bulkContainer = bulkClient.GetContainer("RetailStore", "Orders");

var tasks = orders.Select(o =>
    bulkContainer.CreateItemAsync(o, new PartitionKey(o.CustomerId))
                 .ContinueWith(t =>
                 {
                     if (t.IsFaulted)
                         logger.LogError(t.Exception, "Failed {Id}", o.Id);
                 }));

await Task.WhenAll(tasks);

Change Feed: Event-Driven Architecture for Free

var processor = container
    .GetChangeFeedProcessorBuilder<Order>("order-projector",
        async (context, changes, ct) =>
        {
            foreach (var change in changes)
                await searchIndex.UpsertAsync(change, ct);
        })
    .WithInstanceName(Environment.MachineName)
    .WithLeaseContainer(leaseContainer)
    .Build();

await processor.StartAsync();

The change feed is a persistent, ordered log of every create and update within a partition. It powers materialised views, cache invalidation, and event sourcing without adding Kafka to your stack.

Best Practices and Common Pitfalls

  • Singleton client, Direct mode. Non-negotiable for latency.
  • Always pass the partition key. Omitting it turns a 1 RU point read into a cross-partition fan-out.
  • Log RequestCharge in development. RU consumption is your real performance metric, not milliseconds.
  • Trim your indexing policy. Every indexed path costs RUs on write. Exclude large blobs you never filter on: set ExcludedPaths to /payload/*.
  • Use Session consistency by default. It gives read-your-own-writes at roughly half the RU cost of Strong, and Strong is unavailable across multiple write regions anyway.
  • Denormalise deliberately. There are no joins across containers. Embed data you read together; reference data that is large or changes independently.
  • Avoid unbounded arrays. An item has a 2 MB limit; an order with unlimited line items will eventually fail in production.
  • Use autoscale for spiky traffic, manual RU/s for steady traffic. Autoscale costs 1.5× the manual rate per RU but only bills what you use.
  • Test against the Cosmos DB Emulator (or its Linux container image) in CI rather than a shared cloud account.

Conclusion: Key Takeaways

Building with Azure Cosmos DB in C# is less about learning API surface and more about internalising a handful of design rules. The .NET SDK is excellent and largely gets out of your way — the difficulty lives in modelling.

  • Pick the NoSQL API for new .NET projects.
  • Register CosmosClient as a singleton in Direct mode.
  • Your partition key is the highest-leverage decision you will make; choose for cardinality and query alignment.
  • Prefer point reads and single-partition queries; measure everything in RUs.
  • Use ETags for concurrency, TransactionalBatch for atomicity, bulk mode for ingestion, and the change feed for event-driven flows.

Start with a single region and 400 RU/s, instrument your request charges, and let real usage tell you where to scale. That approach keeps costs sane while giving you a database ready to serve users in New York, London, Toronto, Sydney, and Bangalore with the same millisecond latency.

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments

Popular posts from this blog

.NET MAUI Tutorial 2026: Build Cross-Platform Apps in C#

Learn .NET MAUI in 2026 to build iOS, Android, Windows & Mac apps from one C# codebase. Start this cross-platform tutorial with code examples today. .NET MAUI (Multi-platform App UI) is Microsoft's framework for building native iOS, Android, Windows, and macOS apps from a single C# codebase . If you've ever wanted to ship a mobile app without learning Swift, Kotlin, and Win32 separately, this .NET MAUI tutorial for 2026 is your starting point. In this guide you'll learn what .NET MAUI is, why it matters for cross-platform app development in C#, and how to build your first working app — with runnable code examples and the best practices senior engineers actually use in production. What Is .NET MAUI and Why Use It in 2026? .NET MAUI is the evolution of Xamarin.Forms, fully integrated into the modern .NET runtime. With one project and one language — C# — you target four platforms. The framework compiles to native UI controls on each device, so a button on iOS...

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...