Azure Cosmos DB — deep dive
Cosmos rewards partition-aware design and punishes everything else in Request Units. This provider is built around that reality: the model declares partitioning up front, queries pin partitions automatically when filters allow, and every mapping decision flows through one serializer contract so documents and queries cannot drift apart.
The serializer contract
The provider ships CosmosEntitySerializer — System.Text.Json (web defaults, camelCase) extended
with the modeling vocabulary, and registered as the SDK's CosmosLinqSerializer so LINQ member
translation asks the same contract:
[StoredAs("display_name")]— the document storesdisplay_name, and filters/sorts/projections on the member translate todisplay_name. End to end, by construction.[Unmapped]— the member never serializes, and a query touching it throws naming the member (a filter that can only match nothing is a bug, not an empty result).Converts<TMember, TStored>(…)— declared on the model builder, type-level by design: the SDK serializes filter constants by type, so only a type-level converter keepsx.Status == Status.Opencomparing the stored representation.
Partitioning
model.Entity<Order>(entity => entity
.Container("orders")
.PartitionKey(x => x.Tenant) // flat…
.PartitionKey(x => x.Region)); // …or chained: hierarchical multi-hash (≤ 3 levels)
- Partition inference: filters are analyzed for AND-reachable equality on the partition-key
member (captured variables included). A pinned query runs single-partition — the cheapest RU
path — and
Explain()reportsPartitionScoped. - Hierarchical keys (
[PartitionKey(Order = n)]or chained fluent calls): the container is created multi-hash, point writes build the multi-value key transparently, inference declines (fans out) rather than guessing partial prefixes. - Point writes always carry the key read from the entity — you never pass a
PartitionKeyby hand.
Writes, ETags, patches
- Commits flush as batched point writes with bulk execution enabled.
[ConcurrencyToken]on astringmember (carrying_etag) turnsModify/Mergeinto conditional replaces (If-Match) — see Concurrency.UpdateManyAsyncrenders patch operations (Set,Increment, array append/remove) — the provider queries the affected ids and patches per item, the closest native form Cosmos offers.BeginTransactionAsyncgives aTransactionalBatch— single partition, enforced with guidance.
What is honestly rejected
- Typed
GroupByAsync: the SDK's LINQ emitsSELECT VALUE {…}, which Cosmos SQL cannot combine withGROUP BY. The provider validates the shape (so the contract stays uniform) and throws pointing to the alternatives. A hand-built SQL pushdown is on the roadmap. - Cross-container UNION: no native mechanism exists; combine branches client-side, consciously.
- Per-document data migrations on hierarchical-key entities point to
Run(...)(there is no single partition field to read back).
TTL
[TimeToLive(seconds)] / .TimeToLive(span) sets the container's DefaultTimeToLive: documents
expire that long after their last write unless they override it. See
Time to live for the cross-store semantics table.
Registration recap
services.AddCosmosDatabase(connectionString, databaseName, model => { /* entities */ });
services.AddCosmosRepositories();
services.AddCosmosMigrations(assemblies);
// manual hosting: CosmosClientFactory.Create(connectionString, model) — same serializer contract