Benday.CosmosDb: Easier App Dev using C# and Cosmos Db

August 08, 2025
Cover Image

Working with Azure Cosmos DB usually means re-creating the same scaffolding: ids, partition keys, discriminators, DI wiring, RU logging, batch utilities… rinse, repeat.
Benday.CosmosDb packages those patterns so you can focus on your domain model and queries—not SDK boilerplate.

What you get

  • Domain & repo base classes that standardize id, partition key, _etag, and a discriminator for safe type filtering in shared containers.
  • Owned-item patterns for multi-tenant apps (partition by owner).
  • DI helpers to register a properly configured CosmosClient (with options for gateway mode, bulk execution, DefaultAzureCredential, etc.).
  • RU logging + cross-partition warnings to keep queries efficient.
  • Batch utilities for chunking big operations.

Install

dotnet add package Benday.CosmosDb

(NuGet package: Benday.CosmosDb — latest listed 4.8.0.)


Quick start: API + Web UI

Below is a typical setup that mirrors the SampleApp.Api/SampleApp.WebUi approach in the repo, with your library doing most of the heavy lifting.

1) Define a model (owned item)

using Benday.CosmosDb.DomainModels;

public class TodoItem : OwnedItemBase // PartitionKey == OwnerId
{
    public string Title { get; set; } = "";
    public bool IsDone { get; set; }
}

2) Create a repository (owner-aware)

using Benday.CosmosDb.Repositories;
using Microsoft.Azure.Cosmos;
using Microsoft.Extensions.Options;

public class TodoRepository : CosmosOwnedItemRepository<TodoItem>
{
    public TodoRepository(
        IOptions<CosmosRepositoryOptions<TodoItem>> options,
        CosmosClient client) : base(options, client) { }
}

Why OwnedItemBase + CosmosOwnedItemRepository<T>?
Your library bakes in the “owned-by-user/tenant” pattern so the partition key is the owner id and the repository/query helpers automatically use it—clean, predictable RU usage without hand-rolling partition logic.

3) Configure Cosmos in appsettings.json

{
  "Cosmos": {
    "Endpoint": "https://<your-account>.documents.azure.com:443/",
    "AccountKey": "<secret>",
    "DatabaseName": "AppDb",
    "ContainerName": "AppContainer",
    "PartitionKey": "/ownerId",
    "CreateStructures": true,
    "DatabaseThroughput": 400,
    "UseGatewayMode": false,
    "AllowBulkExecution": true,
    "UseHierarchicalPartitionKey": false,
    "UseDefaultAzureCredential": false
  }
}

4) Wire up DI in Program.cs (API or Web UI)

using Benday.CosmosDb.Utilities;
using Benday.CosmosDb.Repositories;

var builder = WebApplication.CreateBuilder(args);

// Bind config section to CosmosConfig
var cosmosSection = builder.Configuration.GetSection("Cosmos");
var cosmosConfig = new CosmosConfig(
    accountKey: cosmosSection["AccountKey"]!,
    endpoint: cosmosSection["Endpoint"]!,
    databaseName: cosmosSection["DatabaseName"]!,
    containerName: cosmosSection["ContainerName"]!,
    partitionKey: cosmosSection["PartitionKey"]!,
    createStructures: bool.Parse(cosmosSection["CreateStructures"] ?? "false"),
    databaseThroughput: int.Parse(cosmosSection["DatabaseThroughput"] ?? "400"),
    useGatewayMode: bool.Parse(cosmosSection["UseGatewayMode"] ?? "false"),
    useHierarchicalPartitionKey: bool.Parse(cosmosSection["UseHierarchicalPartitionKey"] ?? "false"),
    allowBulkExecution: bool.Parse(cosmosSection["AllowBulkExecution"] ?? "true"),
    useDefaultAzureCredential: bool.Parse(cosmosSection["UseDefaultAzureCredential"] ?? "false")
);

// Registers CosmosClient as a singleton, using options from CosmosConfig
builder.Services.ConfigureCosmosClient(cosmosConfig);

// Register repository options for your model
builder.Services.Configure<CosmosRepositoryOptions<TodoItem>>(o =>
{
    o.DatabaseName = cosmosConfig.DatabaseName;
    o.ContainerName = cosmosConfig.ContainerName;
});

// Register your repo (and optionally a service layer)
builder.Services.AddScoped<TodoRepository>();

var app = builder.Build();
app.MapGet("/", () => "OK");
app.Run();

5) Use the repository in an API endpoint

app.MapPost("/api/todos", async (TodoRepository repo, TodoItem dto, HttpContext ctx) =>
{
    // assume you’ve resolved the owner id from auth
    var ownerId = ctx.User?.Identity?.Name ?? "demo";
    dto.OwnerId = ownerId;

    await repo.SaveAsync(dto);
    return Results.Created($"/api/todos/{dto.Id}", dto);
});

app.MapGet("/api/todos", async (TodoRepository repo, HttpContext ctx) =>
{
    var ownerId = ctx.User?.Identity?.Name ?? "demo";
    var results = await repo.GetResults(q => q.Where(x => x.OwnerId == ownerId));
    return Results.Ok(results);
});

Optional: Service layer for thinner controllers / pages

Prefer keeping controllers razor-thin? Use a service that composes the repo:

using Benday.CosmosDb.ServiceLayers;

public class TodoService : IOwnedItemService<TodoItem>
{
    private readonly TodoRepository _repo;
    public TodoService(TodoRepository repo) => _repo = repo;

    public Task DeleteAsync(TodoItem item) => _repo.DeleteAsync(item.Id);
    public Task<TodoItem?> GetByIdAsync(string id) => _repo.GetByIdAsync(id);
    public Task SaveAsync(TodoItem item) => _repo.SaveAsync(item);
    // add other list/query methods that encapsulate OwnerId, etc.
}

Register TodoService and call it from API/Web UI—this mirrors the OwnedItemService pattern in your package.


Batch operations (import/migrations)

When you need to save or process lots of items, use the batch helpers:

using Benday.CosmosDb.Utilities;

var batches = BatchUtility.CreateArrayForBatch(items, startIndex: 0, batchSize: 100);

foreach (var batch in batches)
{
    // save/process each chunk
    foreach (var item in batch) { await repo.SaveAsync(item); }
}

This pairs nicely with the package’s bulk execution option and keeps memory/throughput predictable.


Notes on partition keys & credentials

  • Flat partition keys are the default as of v4.0; if you need hierarchical PKs, enable them via config.
  • You can opt into DefaultAzureCredential for managed identities (great for Azure hosting) via config/DI helpers.

Links

Categories: cosmosdb