---
title: "Integrating Azure Cosmos DB with ASP.NET Core Web API: A Practical Walkthrough"  
description: "Learn how to integrate Azure Cosmos DB with ASP.NET Core Web API. This guide covers client setup, repository patterns, CRUD operations, throttling, and local testing with the emulator."  
author: "Ravi Vishwakarma"  
published: 2026-09-17  
updated: 2026-09-17  
canonical: https://answers.mindstick.com/blog/626/integrating-azure-cosmos-db-with-asp-net-core-web-api-a-practical-walkthrough  
category: "Cloud & Databases"  
tags: ["azure", "dotnet", "NoSQL", "microservices", "tutorial"]  
reading_time: 10 minutes  

---

# Integrating Azure Cosmos DB with ASP.NET Core Web API: A Practical Walkthrough

## Why Cosmos DB and ASP.NET Core Make Sense Together

You are building a web service that needs to scale without thinking about sharding. Azure [Cosmos DB](https://answers.mindstick.com/blog/586/azure-cosmos-db-explained-for-beginners-complete-guide) gives you that. ASP.NET Core gives you the runtime. Putting them together is not magic, but it does require a few deliberate steps. Most developers trip up on connection string handling, container provisioning, or the way asynchronous I/O interacts with the Cosmos SDK. This guide skips the marketing fluff and shows you exactly what to wire up, from the NuGet packages to a working CRUD endpoint.

## Prerequisites and Project Setup

Before you write a single line of controller code, make sure your environment is ready. You need an Azure subscription, the .NET 8 SDK installed locally, and a running instance of Visual Studio 2022 or the VS Code equivalent. You also need an existing Cosmos DB account in one of the supported regions. If you do not have one, the Azure portal lets you create a free tier account in minutes. Once that exists, grab the primary connection string from the *Keys* blade. Treat it like a password; do not commit it to source control.

Create a fresh [ASP.NET Core Web API](https://www.mindstick.com/forum/239/net) project. Open your terminal and run:

```sh
dotnet new webapi -n CosmosApi
cd CosmosApi
dotnet add package Microsoft.Azure.Cosmos
dotnet add package Microsoft.Extensions.Configuration.AzureKeyVault
```

The first package pulls in the official Cosmos DB SDK. The second is optional but recommended if you store secrets in Azure Key Vault rather than plain text. After installation, open *Program.cs*. This is where the host builder gets configured. You will register the Cosmos client as a singleton so every request reuses the same underlying TCP connection pool. That saves latency and keeps connection counts low.

## Wiring the Cosmos Client in Dependency Injection

Dependency injection is the backbone of ASP.NET Core. Instead of calling `CosmosClient` directly inside your controllers, you inject it through the constructor. This keeps your business logic testable and your startup sequence clean.

Add the following to your *Program.cs* file. Notice how we read the connection string from configuration, then instantiate the client. We also set a default consistency level to Session, which is a sensible default for most transactional web APIs.

```cs
// Program.cs

using Microsoft.Azure.Cosmos;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);

// Bind the Cosmos DB connection string from appsettings.json
var connectionString = builder.Configuration.GetConnectionString("CosmosDb");

// Register CosmosClient as a singleton service
builder.Services.AddSingleton<CosmosClient>(serviceProvider =>
    new CosmosClient(connectionString)
);

// Optional: register a container client per database/container pair later
builder.Services.AddSingleton<Container>(serviceProvider =>
{
    var client = serviceProvider.GetRequiredService<CosmosClient>();
    // Replace with your actual database and container names
    return client.GetContainer("RetailStore", "Products");
});

var app = builder.Build();

// ... standard middleware pipeline ...
app.MapControllers();
app.Run();
```

If you prefer to keep the container name out of code, you can read it from configuration as well. The key point is that `CosmosClient` is expensive to create, so it stays alive for the lifetime of the application. The `Container` object, by contrast, is lightweight and can be scoped per request if you ever need to switch databases dynamically.

## Designing Your Data Model and Container

Cosmos DB is a document database. Your entities become JSON documents stored in a container. Before you write C# classes, decide on a partition key. This is the most critical design choice. It determines how data is distributed across physical partitions and directly impacts query performance and RU consumption. A poor partition key leads to hot partitions and throttling.

For a typical e-commerce product catalog, the SKU or a tenant identifier works well. Let us assume every product belongs to a specific store, and we use the store ID as the partition key. Here is a minimal POCO that maps to a Cosmos document:

```cs
public class Product
{
    // The partition key must be a top-level property
    [JsonProperty("id")]
    public string Id { get; set; } = Guid.NewGuid().ToString();

    [JsonProperty("storeId")]
    public string StoreId { get; set; } = string.Empty;

    [JsonProperty("name")]
    public string Name { get; set; } = string.Empty;

    [JsonProperty("price")]
    public decimal Price { get; set; }

    [JsonProperty("createdAt")]
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
```

The `[JsonProperty]` attributes align the C# property names with the JSON document structure. Cosmos DB uses the `id` field as the unique document identifier, so we generate a GUID up front. If you are using a different identifier strategy, adjust accordingly, but always ensure the partition key is indexed.

Provisioning the container itself is a one-time Azure CLI or portal task. If you automate deployments, use the Azure Cosmos DB REST API or the .NET SDK to create the container with a throughput setting. A starting point of 400 RU/s is reasonable for a small workload. Do not over-provision; you can scale up later without downtime.

## Building the Repository Layer

Controllers should stay thin. Extract all database interaction into a [repository class](https://www.mindstick.com/interview/2495/class). This isolates Cosmos-specific logic, makes unit testing easier with mocked interfaces, and keeps your HTTP endpoints focused on request/response handling. Below is a repository that supports the basic CRUD operations using the Cosmos SDK's asynchronous methods.

```cs
// IProductRepository.cs

using Microsoft.Azure.Cosmos;

public interface IProductRepository
{
    Task<IEnumerable<Product>> GetAllAsync(string storeId);
    Task<Product> GetByIdAsync(string id);
    Task CreateAsync(Product product);
    Task UpdateAsync(string id, Product product);
    Task DeleteAsync(string id);
}
```

Now implement the interface. The key method to understand is `CreateItemAsync`, which inserts a new document and returns the generated resource link. For updates, use `ReplaceItemAsync`, which overwrites the entire document. Partial updates require a different approach, typically fetching the document, modifying the fields in memory, and writing it back. Cosmos DB does not support atomic field-level patching in the same way relational databases support `UPDATE ... SET`, so plan your mutation strategy accordingly.

```cs
// ProductRepository.cs

using Microsoft.Azure.Cosmos;
using Microsoft.Extensions.Options;

public class ProductRepository : IProductRepository
{
    private readonly Container _container;

    public ProductRepository(Container container)
    {
        _container = container;
    }

    public async Task<IEnumerable<Product>> GetAllAsync(string storeId)
    {
        // Build a query filter to limit results to a specific store
        var query = new QueryDefinition("SELECT * FROM c WHERE c.storeId = @storeId")
            .WithParameter("@storeId", storeId);

        using var feedIterator = _container.GetItemQueryIterator<Product>(query);
        var results = new List<Product>();

        while (feedIterator.HasMoreResults)
        {
            var response = await feedIterator.ReadNextAsync();
            results.AddRange(response);
        }

        return results;
    }

    public async Task<Product> GetByIdAsync(string id)
    {
        // Point read by document ID; fastest way to retrieve a single item
        var response = await _container.ReadItemAsync<Product>(id, new PartitionKey(id));
        return response.Resource;
    }

    public async Task CreateAsync(Product product)
    {
        // Insert a new document; Cosmos generates the id if omitted
        await _container.CreateItemAsync(product, new PartitionKey(product.StoreId));
    }

    public async Task UpdateAsync(string id, Product product)
    {
        // Replace the entire document. Ensure the partition key matches.
        await _container.ReplaceItemAsync(product, id, new PartitionKey(product.StoreId));
    }

    public async Task DeleteAsync(string id)
    {
        // Remove the document by its ID and partition key
        await _container.DeleteItemAsync(id, new PartitionKey(id));
    }
}
```

Notice the use of `PartitionKey` in every operation. The SDK needs this to route the request to the correct physical partition. For `GetAllAsync`, we filter by `storeId` because that is our chosen partition key. If you query without a filter on the partition key, Cosmos performs a cross-partition query, which burns more Request Units and can time out under load.

## Creating the Web API Controller

With the repository registered in DI, the controller becomes straightforward. Inject `IProductRepository`, validate the incoming model state, and delegate to the repository. Return appropriate HTTP status codes: `201` for creation, `404` when a document is missing, and `400` for validation failures.

```cs
// ProductsController.cs

using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductRepository _repository;

    public ProductsController(IProductRepository repository)
    {
        _repository = repository;
    }

    [HttpGet("{storeId}")]
    public async Task<ActionResult<IEnumerable<Product>>> GetByStore(string storeId)
    {
        var products = await _repository.GetAllAsync(storeId);
        if (!products.Any())
        {
            return NotFound();
        }

        return Ok(products);
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<Product>> GetById(string id)
    {
        var product = await _repository.GetByIdAsync(id);
        if (product == null)
        {
            return NotFound();
        }

        return Ok(product);
    }

    [HttpPost]
    public async Task<ActionResult<Product>> Create([FromBody] Product product)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        await _repository.CreateAsync(product);
        return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
    }

    [HttpPut("{id}")]
    public async Task<IActionResult> Update(string id, [FromBody] Product product)
    {
        if (id != product.Id)
        {
            return BadRequest();
        }

        var existing = await _repository.GetByIdAsync(id);
        if (existing == null)
        {
            return NotFound();
        }

        await _repository.UpdateAsync(id, product);
        return NoContent();
    }

    [HttpDelete("{id}")]
    public async Task<IActionResult> Delete(string id)
    {
        await _repository.DeleteAsync(id);
        return NoContent();
    }
}
```

The `CreatedAtAction` helper returns a `201` response with a `Location` header pointing to the newly created resource. This is REST-compliant and helps clients discover endpoints without hard-coding URLs. For bulk operations, you would typically expose a stored procedure or use the SDK's bulk executor, but that is beyond the scope of this walkthrough.

## Handling Throughput and Throttling

Every operation against Cosmos DB consumes Request Units, or RUs. If your container is provisioned with a fixed throughput, you will eventually hit throttling when traffic spikes. The default consistency level also affects RU cost; strong consistency is the most expensive. Session consistency, which we used in the client setup, strikes a balance between performance and data freshness for most web applications.

To monitor RU consumption, enable diagnostic logging in Azure Monitor. Set up an alert on the `Total RU/s` metric. If you consistently exceed your provisioned limit, increase the throughput in the Azure portal. Alternatively, switch to autoscale, which adjusts RU/s automatically based on demand. Autoscale is cost-effective for workloads with unpredictable traffic patterns.

Another mitigation strategy is to cache frequently accessed data in Redis or in-memory. A read-through cache reduces the number of round trips to Cosmos DB. Just remember to invalidate the cache on writes, or your users will see stale product prices.

## Testing the Integration Locally

You do not need to deploy to Azure to verify your code compiles and your queries behave as expected. The Cosmos DB [emulator](https://www.mindstick.com/articles/1443/speeding-up-android-emulator) runs as a Docker container on your development machine. It mimics the service API and stores data in a local directory. Point your connection string to the emulator endpoint, and you can run integration tests against a real database without incurring cloud costs.

Start the emulator with Docker:

```sh
docker run -d -p 8081:8081 --name cosmos-emulator \
  mcr.microsoft.com/cosmosdb/emulator:latest
```

Then update your *appsettings.json* to use the emulator URL and a dummy key. Run your API, hit the endpoints with `curl` or Postman, and inspect the response times. If a query returns empty unexpectedly, check the partition key filter. A missing filter on a large container can return zero results if the query times out before completing.

## Common Pitfalls and How to Avoid Them

One frequent mistake is treating Cosmos DB like a relational database. There are no joins, no foreign keys, and no transactions across multiple containers. If you need relational integrity, either denormalize your data or use a different service. Another pitfall is ignoring the 429 status code. When you receive a 429, back off exponentially and retry with the `Retry-After` header. Blindly retrying immediately will only worsen the throttling.

Index management is also crucial. Every property you query against should have a corresponding index policy. By default, Cosmos creates a range index on all properties, but custom composite indexes can speed up specific query patterns. Review your query workload, then tune the index settings in the Azure portal or via the SDK. Over-indexing wastes RU and storage; under-indexing causes query failures.

## Wrapping Up

You now have a working ASP.NET Core Web API backed by Azure Cosmos DB. The architecture separates concerns cleanly: configuration lives in *Program.cs*, data access sits in a repository, and HTTP handling stays in the controller. From here, you can extend the API with authentication, pagination, or event-driven updates via Azure Functions. Just remember to keep your partition key strategy consistent, monitor your RU consumption, and test against the emulator before pushing changes to production.

---

Original Source: https://answers.mindstick.com/blog/626/integrating-azure-cosmos-db-with-asp-net-core-web-api-a-practical-walkthrough

Copyright © MindStick Software Pvt. Ltd. This Markdown version is provided for developers, AI systems, and offline reading.
