Skip to main content
The Intent.CosmosDB module provides complete Azure Cosmos DB integration, generating repositories, documents, and configuration for NoSQL document-based persistence.

Overview

This module generates:
  • Cosmos DB document classes
  • Repository implementations
  • Unit of Work pattern
  • Container configuration
  • Multi-tenancy support
  • Optimistic concurrency control
  • Cosmos DB client setup

Installation

Intent.CosmosDB

Key Features

  • Document-based storage - JSON document persistence
  • Partitioning support - Horizontal scaling via partition keys
  • Global distribution - Multi-region replication
  • Automatic indexing - Query optimization
  • Change feed - Real-time event processing
  • Multi-tenancy - Tenant isolation

Configuration

Connection Settings

appsettings.json
{
  "CosmosDb": {
    "ConnectionString": "AccountEndpoint=https://your-account.documents.azure.com:443/;AccountKey=your-key==",
    "DatabaseName": "MyApplication",
    "Containers": {
      "Customers": {
        "ContainerName": "Customers",
        "PartitionKey": "/tenantId",
        "Throughput": 400
      },
      "Orders": {
        "ContainerName": "Orders",
        "PartitionKey": "/customerId",
        "Throughput": 400
      }
    }
  }
}
appsettings.json
{
  "CosmosDb": {
    "AccountEndpoint": "https://your-account.documents.azure.com:443/",
    "DatabaseName": "MyApplication",
    "UseManagedIdentity": true
  }
}

Module Settings

Ensures updates are applied only when document ETags match, for improved concurrency management.Prevents lost updates when multiple clients modify the same document.Default: true
Enables a convention to convert enums to/from strings.Benefits:
  • More readable JSON
  • Better compatibility with other systems
  • Easier debugging
Default: false
Specify which authentication methods to use:
  • Key-based - Connection string with account key
  • Managed Identity - Azure Managed Identity (recommended for production)
Default: key-based

Document Generation

Domain Entity to Document

Customer Document
public class CustomerDocument : ICosmosDBDocument<Customer, CustomerDocument>
{
    [JsonProperty("id")]
    public string Id { get; set; }

    [JsonProperty("tenantId")]
    public string TenantId { get; set; }

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

    [JsonProperty("email")]
    public string Email { get; set; }

    [JsonProperty("isVIP")]
    public bool IsVIP { get; set; }

    [JsonProperty("createdDate")]
    public DateTime CreatedDate { get; set; }

    [JsonProperty("_etag")]
    public string ETag { get; set; }

    public Customer ToEntity()
    {
        return new Customer
        {
            Id = Guid.Parse(Id),
            Name = Name,
            Email = Email,
            IsVIP = IsVIP,
            CreatedDate = CreatedDate
        };
    }

    public static CustomerDocument FromEntity(Customer entity)
    {
        return new CustomerDocument
        {
            Id = entity.Id.ToString(),
            Name = entity.Name,
            Email = entity.Email,
            IsVIP = entity.IsVIP,
            CreatedDate = entity.CreatedDate
        };
    }
}

Complex Documents

Order Document
public class OrderDocument : ICosmosDBDocument<Order, OrderDocument>
{
    [JsonProperty("id")]
    public string Id { get; set; }

    [JsonProperty("customerId")]
    public string CustomerId { get; set; }

    [JsonProperty("orderNumber")]
    public string OrderNumber { get; set; }

    [JsonProperty("items")]
    public List<OrderItemDocument> Items { get; set; }

    [JsonProperty("shippingAddress")]
    public AddressDocument ShippingAddress { get; set; }

    [JsonProperty("status")]
    public string Status { get; set; }

    [JsonProperty("total")]
    public decimal Total { get; set; }

    [JsonProperty("orderDate")]
    public DateTime OrderDate { get; set; }

    [JsonProperty("_etag")]
    public string ETag { get; set; }

    public Order ToEntity()
    {
        var order = new Order
        {
            Id = Guid.Parse(Id),
            CustomerId = Guid.Parse(CustomerId),
            OrderNumber = OrderNumber,
            Status = Enum.Parse<OrderStatus>(Status),
            Total = Total,
            OrderDate = OrderDate
        };

        foreach (var itemDoc in Items)
        {
            order.Items.Add(itemDoc.ToEntity());
        }

        order.ShippingAddress = ShippingAddress.ToEntity();

        return order;
    }
}

public class OrderItemDocument
{
    [JsonProperty("productId")]
    public string ProductId { get; set; }

    [JsonProperty("productName")]
    public string ProductName { get; set; }

    [JsonProperty("quantity")]
    public int Quantity { get; set; }

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

    public OrderItem ToEntity()
    {
        return new OrderItem
        {
            ProductId = Guid.Parse(ProductId),
            ProductName = ProductName,
            Quantity = Quantity,
            Price = Price
        };
    }
}

Repository Implementation

Base Repository

Cosmos Repository Base
public abstract class CosmosDBRepositoryBase<TDomain, TDocument, TDocumentInterface>
    where TDocument : TDocumentInterface
    where TDocumentInterface : ICosmosDBDocument<TDomain, TDocument>
{
    protected readonly Container _container;
    protected readonly ICosmosDBUnitOfWork _unitOfWork;

    protected CosmosDBRepositoryBase(
        Container container,
        ICosmosDBUnitOfWork unitOfWork)
    {
        _container = container;
        _unitOfWork = unitOfWork;
    }

    public ICosmosDBUnitOfWork UnitOfWork => _unitOfWork;

    public virtual void Add(TDomain entity)
    {
        var document = TDocumentInterface.FromEntity(entity);
        _unitOfWork.TrackForCreate(document, _container);
    }

    public virtual void Update(TDomain entity)
    {
        var document = TDocumentInterface.FromEntity(entity);
        _unitOfWork.TrackForUpdate(document, _container);
    }

    public virtual void Remove(TDomain entity)
    {
        var document = TDocumentInterface.FromEntity(entity);
        _unitOfWork.TrackForDelete(document, _container);
    }

    public virtual async Task<TDomain?> FindByIdAsync(
        string id,
        string partitionKey,
        CancellationToken cancellationToken = default)
    {
        try
        {
            var response = await _container.ReadItemAsync<TDocument>(
                id,
                new PartitionKey(partitionKey),
                cancellationToken: cancellationToken);

            return response.Resource.ToEntity();
        }
        catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
        {
            return default;
        }
    }

    public virtual async Task<List<TDomain>> FindAllAsync(
        CancellationToken cancellationToken = default)
    {
        var query = _container.GetItemQueryIterator<TDocument>();
        var results = new List<TDomain>();

        while (query.HasMoreResults)
        {
            var response = await query.ReadNextAsync(cancellationToken);
            results.AddRange(response.Select(doc => doc.ToEntity()));
        }

        return results;
    }

    public virtual async Task<List<TDomain>> QueryAsync(
        QueryDefinition queryDefinition,
        CancellationToken cancellationToken = default)
    {
        var query = _container.GetItemQueryIterator<TDocument>(queryDefinition);
        var results = new List<TDomain>();

        while (query.HasMoreResults)
        {
            var response = await query.ReadNextAsync(cancellationToken);
            results.AddRange(response.Select(doc => doc.ToEntity()));
        }

        return results;
    }
}

Entity Repository

Customer Repository
public interface ICustomerRepository : ICosmosDBRepository<Customer>
{
    Task<Customer?> FindByIdAsync(Guid id, CancellationToken cancellationToken = default);
    Task<Customer?> FindByEmailAsync(string email, CancellationToken cancellationToken = default);
    Task<List<Customer>> FindVIPCustomersAsync(CancellationToken cancellationToken = default);
}

public class CustomerRepository : CosmosDBRepositoryBase<Customer, CustomerDocument, ICustomerDocument>, ICustomerRepository
{
    public CustomerRepository(
        Container container,
        ICosmosDBUnitOfWork unitOfWork) 
        : base(container, unitOfWork)
    {
    }

    public async Task<Customer?> FindByIdAsync(
        Guid id,
        CancellationToken cancellationToken = default)
    {
        return await FindByIdAsync(
            id.ToString(),
            id.ToString(), // Assuming partition key is the id
            cancellationToken);
    }

    public async Task<Customer?> FindByEmailAsync(
        string email,
        CancellationToken cancellationToken = default)
    {
        var queryDefinition = new QueryDefinition(
            "SELECT * FROM c WHERE c.email = @email")
            .WithParameter("@email", email);

        var results = await QueryAsync(queryDefinition, cancellationToken);
        return results.FirstOrDefault();
    }

    public async Task<List<Customer>> FindVIPCustomersAsync(
        CancellationToken cancellationToken = default)
    {
        var queryDefinition = new QueryDefinition(
            "SELECT * FROM c WHERE c.isVIP = @isVIP")
            .WithParameter("@isVIP", true);

        return await QueryAsync(queryDefinition, cancellationToken);
    }
}

Usage Examples

Basic CRUD

CRUD Operations
public class CustomerService
{
    private readonly ICustomerRepository _repository;

    public async Task<Customer> CreateCustomerAsync(
        string name, 
        string email)
    {
        var customer = new Customer(name, email);
        
        _repository.Add(customer);
        await _repository.UnitOfWork.SaveChangesAsync();
        
        return customer;
    }

    public async Task<Customer?> GetCustomerAsync(Guid id)
    {
        return await _repository.FindByIdAsync(id);
    }

    public async Task UpdateCustomerAsync(Guid id, string name)
    {
        var customer = await _repository.FindByIdAsync(id);
        if (customer != null)
        {
            customer.UpdateName(name);
            _repository.Update(customer);
            await _repository.UnitOfWork.SaveChangesAsync();
        }
    }

    public async Task DeleteCustomerAsync(Guid id)
    {
        var customer = await _repository.FindByIdAsync(id);
        if (customer != null)
        {
            _repository.Remove(customer);
            await _repository.UnitOfWork.SaveChangesAsync();
        }
    }
}

Advanced Queries

Advanced Queries
// Query with parameters
var queryDef = new QueryDefinition(
    "SELECT * FROM c WHERE c.createdDate >= @startDate AND c.createdDate <= @endDate")
    .WithParameter("@startDate", startDate)
    .WithParameter("@endDate", endDate);

var customers = await _repository.QueryAsync(queryDef);

// Query with pagination
var queryOptions = new QueryRequestOptions
{
    MaxItemCount = 20 // Page size
};

var query = _container.GetItemQueryIterator<CustomerDocument>(
    queryDef,
    requestOptions: queryOptions);

var page = await query.ReadNextAsync();
var continuationToken = page.ContinuationToken; // For next page

Cross-Partition Queries

Cross-Partition Query
var queryDefinition = new QueryDefinition(
    "SELECT * FROM c WHERE c.isVIP = @isVIP")
    .WithParameter("@isVIP", true);

var queryOptions = new QueryRequestOptions
{
    MaxItemCount = 100,
    MaxConcurrency = -1 // Unlimited parallelism
};

var query = _container.GetItemQueryIterator<CustomerDocument>(
    queryDefinition,
    requestOptions: queryOptions);

var allResults = new List<Customer>();
while (query.HasMoreResults)
{
    var response = await query.ReadNextAsync();
    allResults.AddRange(response.Select(doc => doc.ToEntity()));
}

Partitioning

Partition Key Design

Partition Key
// Good partition keys:
// - TenantId (for multi-tenant apps)
// - UserId (for user-specific data)
// - Date (for time-series data)
// - CustomerId (for customer data)

public class OrderDocument
{
    [JsonProperty("id")]
    public string Id { get; set; }

    [JsonProperty("customerId")] // Partition key
    public string CustomerId { get; set; }

    // Other properties...
}

// Container configuration
var containerProperties = new ContainerProperties
{
    Id = "Orders",
    PartitionKeyPath = "/customerId"
};

Multi-Tenancy

Multi-Tenant Setup
public class TenantDocument
{
    [JsonProperty("id")]
    public string Id { get; set; }

    [JsonProperty("tenantId")] // Partition key
    public string TenantId { get; set; }

    [JsonProperty("data")]
    public string Data { get; set; }
}

// Query within tenant
var queryDef = new QueryDefinition(
    "SELECT * FROM c WHERE c.tenantId = @tenantId")
    .WithParameter("@tenantId", currentTenantId);

var queryOptions = new QueryRequestOptions
{
    PartitionKey = new PartitionKey(currentTenantId)
};

Optimistic Concurrency

Concurrency Control
public async Task UpdateWithConcurrencyAsync(Customer customer)
{
    var document = CustomerDocument.FromEntity(customer);

    var requestOptions = new ItemRequestOptions
    {
        IfMatchEtag = document.ETag // Only update if ETag matches
    };

    try
    {
        await _container.ReplaceItemAsync(
            document,
            document.Id,
            new PartitionKey(document.TenantId),
            requestOptions);
    }
    catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.PreconditionFailed)
    {
        throw new ConcurrencyException(
            "The document was modified by another process");
    }
}

Performance Optimization

Indexing

Configure indexes for frequently queried properties to improve performance.

Partition Strategy

Choose partition keys that distribute load evenly across physical partitions.

Query Optimization

Use SELECT with specific fields instead of SELECT * to reduce RU consumption.

Caching

Cache frequently accessed documents to reduce Cosmos DB calls.

Request Units (RU)

Monitor RUs
var response = await _container.ReadItemAsync<CustomerDocument>(
    id,
    new PartitionKey(partitionKey));

var requestCharge = response.RequestCharge;
_logger.LogInformation("Request consumed {RequestCharge} RUs", requestCharge);

Change Feed

Change Feed Processor
var changeFeedProcessor = _container
    .GetChangeFeedProcessorBuilder<CustomerDocument>(
        "customerProcessor",
        HandleChangesAsync)
    .WithInstanceName("consoleHost")
    .WithLeaseContainer(_leaseContainer)
    .Build();

await changeFeedProcessor.StartAsync();

async Task HandleChangesAsync(
    IReadOnlyCollection<CustomerDocument> changes,
    CancellationToken cancellationToken)
{
    foreach (var document in changes)
    {
        _logger.LogInformation(
            "Customer changed: {CustomerId}", 
            document.Id);
        
        // Process change
        await ProcessCustomerChangeAsync(document, cancellationToken);
    }
}

Best Practices

  1. Design for scale - Choose partition keys that distribute data evenly
  2. Minimize cross-partition queries - They consume more RUs
  3. Use point reads - Most efficient way to read documents
  4. Implement retry logic - Handle rate limiting (429 status)
  5. Monitor RU consumption - Optimize queries based on RU usage
  6. Use bulk operations - For high-volume data operations

Additional Resources

Build docs developers (and LLMs) love