Infrastructure.Data.CosmosDb
10.2.0
Infrastructure.Data.CosmosDb 10.2.1
Additional Details Version 10.2.0 throws ArgumentException in valid configurations: (1) when
reusing a pre-existing container whose partition key path differs from the
one in settings, and (2) at startup when a partition key path segment cannot
be matched to a C# property on the entity. Both are fixed in 10.2.1, which is
API-compatible with 10.2.0 — just upgrade to 10.2.1 or later.
See the version list below for details.
dotnet add package Infrastructure.Data.CosmosDb --version 10.2.0
NuGet\Install-Package Infrastructure.Data.CosmosDb -Version 10.2.0
<PackageReference Include="Infrastructure.Data.CosmosDb" Version="10.2.0" />
<PackageVersion Include="Infrastructure.Data.CosmosDb" Version="10.2.0" />
<PackageReference Include="Infrastructure.Data.CosmosDb" />
paket add Infrastructure.Data.CosmosDb --version 10.2.0
#r "nuget: Infrastructure.Data.CosmosDb, 10.2.0"
#:package Infrastructure.Data.CosmosDb@10.2.0
#addin nuget:?package=Infrastructure.Data.CosmosDb&version=10.2.0
#tool nuget:?package=Infrastructure.Data.CosmosDb&version=10.2.0
Infrastructure.Data.CosmosDb
A .NET 10.0 generic repository base class for Azure Cosmos DB, built on the Microsoft.Azure.Cosmos SDK v3.
Installation
dotnet add package Infrastructure.Data.CosmosDb
Configuration
Add the connection settings to your appsettings.json:
{
"CosmosDb": {
"Endpoint": "https://<your-account>.documents.azure.com:443/",
"Key": "<your-primary-key>",
"DatabaseId": "MyDatabase",
"CollectionId": "MyCollection",
"PartitionKey": "/id"
}
}
Register the settings in your application startup:
services.Configure<Settings>(configuration.GetSection("CosmosDb"));
Usage
1. Create your entity
The entity must expose a string property that maps to the Cosmos DB document id. The repository resolves it in this order:
- A property named
Id - A property named
id - Any property decorated with
[JsonPropertyName("id")]
The CosmosClient serializer must map the chosen property to the JSON field id. Using a camelCase naming policy covers options 1 and 2 automatically; [JsonPropertyName("id")] covers option 3 regardless of naming policy.
// Option 1 — property named Id (most common)
public class Order
{
public string Id { get; set; }
public string CustomerId { get; set; }
public decimal Total { get; set; }
}
// Option 2 — property named id
public class Order
{
public string id { get; set; }
}
// Option 3 — custom property name with JSON attribute
// With AddCosmosClient() (STJ):
public class Order
{
[JsonPropertyName("id")]
public string DocumentId { get; set; }
}
// With AddCosmosClientWithNewtonsoft():
public class Order
{
[Newtonsoft.Json.JsonProperty("id")]
public string DocumentId { get; set; }
}
2. Create your repository
Inherit from Repository<TEntity> and inject CosmosClient and IOptions<Settings>:
public class OrderRepository : Repository<Order>
{
public OrderRepository(CosmosClient client, IOptions<Settings> options)
: base(client, options) { }
}
To override the collection or partition key defined in configuration:
public class ProductRepository : Repository<Product>
{
public ProductRepository(CosmosClient client, IOptions<Settings> options)
: base(client, options, collectionId: "Products", partitionKey: "/category") { }
}
For containers without a partition key, pass an empty string:
public class LogRepository : Repository<LogEntry>
{
public LogRepository(CosmosClient client, IOptions<Settings> options)
: base(client, options, collectionId: "Logs", partitionKey: "") { }
}
3. Register and use
Register CosmosClient as a singleton using one of the provided extension methods:
// Program.cs
services.Configure<Settings>(configuration.GetSection("CosmosDb"));
// Option A — System.Text.Json with camelCase (recommended for new projects)
// Supports [JsonPropertyName] attributes
services.AddCosmosClient();
// Option B — Newtonsoft.Json with camelCase (for existing projects or preference)
// Supports [Newtonsoft.Json.JsonProperty] attributes
services.AddCosmosClientWithNewtonsoft();
// Both accept an optional delegate for additional configuration
services.AddCosmosClient(opt => opt.ConnectionMode = ConnectionMode.Gateway);
services.AddScoped<OrderRepository>();
// Usage
public class OrderService
{
private readonly OrderRepository _repository;
public OrderService(OrderRepository repository) => _repository = repository;
// When PartitionKey is "/id": the partition key value equals the document id.
public Task<Order> GetOrder(string id) => _repository.GetByID(id, id);
// When PartitionKey is "/customerId": supply the partition key value explicitly.
// public Task<Order> GetOrder(string id, string customerId)
// => _repository.GetByID(id, customerId);
public Task<IEnumerable<Order>> GetByCustomer(string customerId)
=> _repository.GetAll(o => o.CustomerId == customerId);
public async Task<string> CreateOrder(Order order)
=> (await _repository.Add(order))?.ToString();
public Task UpdateOrder(Order order) => _repository.Update(order, order.Id);
// When PartitionKey is "/id": the partition key value equals the document id.
public Task DeleteOrder(string id) => _repository.DeleteBy(id, id);
}
Overriding behavior
All data access operations are delegated to protected virtual methods, making it easy to customize or test without a real Cosmos DB connection:
| Public method | Protected override |
|---|---|
GetByID(dynamic id) |
ReadItemInternalAsync(string id) — only for containers without a partition key |
GetByID(string id, string pk) |
ReadItemInternalAsync(string id, PartitionKey pk) |
GetAll() |
QueryAllItemsInternalAsync |
GetAll(predicate) |
QueryItemsInternalAsync |
Add |
CreateItemInternalAsync |
Update |
ReplaceItemInternalAsync |
DeleteBy(dynamic id) |
DeleteItemInternalAsync(string id) — only for containers without a partition key |
DeleteBy(string id, string pk) |
DeleteItemInternalAsync(string id, PartitionKey pk) |
DeleteBy(TEntity entity) |
DeleteItemInternalAsync(TEntity entity, string id) |
public class CachedOrderRepository : Repository<Order>
{
private readonly IMemoryCache _cache;
public CachedOrderRepository(CosmosClient client, IOptions<Settings> options, IMemoryCache cache)
: base(client, options) => _cache = cache;
// Called by GetByID(string id, string partitionKeyValue).
protected override async Task<Order> ReadItemInternalAsync(string id, PartitionKey partitionKey)
{
return await _cache.GetOrCreateAsync(id, _ => base.ReadItemInternalAsync(id, partitionKey));
}
}
Testing without Cosmos DB
Use the parameterless protected constructor and override the internal methods with an in-memory store:
class TestOrderRepository : Repository<Order>
{
private readonly Dictionary<string, Order> _store = new();
public TestOrderRepository() : base() { }
protected override Task<Order> ReadItemInternalAsync(string id)
{
_store.TryGetValue(id, out var item);
return Task.FromResult(item);
}
// Called by GetByID(string id, string partitionKeyValue).
protected override Task<Order> ReadItemInternalAsync(string id, PartitionKey partitionKey)
{
_store.TryGetValue(id, out var item);
return Task.FromResult(item);
}
protected override Task<dynamic> CreateItemInternalAsync(Order item)
{
if (string.IsNullOrEmpty(item.Id)) item.Id = Guid.NewGuid().ToString();
_store[item.Id] = item;
return Task.FromResult<dynamic>(item.Id);
}
protected override Task DeleteItemInternalAsync(string id)
{
_store.Remove(id);
return Task.CompletedTask;
}
// Called by DeleteBy(string id, string partitionKeyValue).
protected override Task DeleteItemInternalAsync(string id, PartitionKey partitionKey)
{
_store.Remove(id);
return Task.CompletedTask;
}
protected override Task DeleteItemInternalAsync(Order entity, string id)
{
_store.Remove(id);
return Task.CompletedTask;
}
}
Running integration tests
Integration tests connect to a real Cosmos DB Emulator and are skipped automatically if it is not available — they never block the build.
Option 1 — Windows native emulator
winget install Microsoft.Azure.CosmosEmulator
# start via Start Menu or:
& "C:\Program Files\Azure Cosmos DB Emulator\CosmosDB.Emulator.exe"
Option 2 — Docker
docker compose up -d
Running the tests
# unit tests (always available, no emulator needed)
dotnet test Infrastructure.Data.CosmosDb.Tests
# integration tests (skipped if emulator is not running)
dotnet test Infrastructure.Data.CosmosDb.IntegrationTests
Notes
- The database and container are created automatically on first use if they do not exist.
- Default container throughput is 1000 RU/s. Override
CreateCollectionIfNotExistsAsyncto customize. Addgenerates a GUID id if the entity's id property is null or empty.- The partition key value for
Add,UpdateandDeleteBy(entity)is resolved automatically from the entity via reflection on the configuredPartitionKeypath. Both slash notation (e.g./tenantIdor/address/city) and dot notation (e.g./address.city) are supported. Property lookup is case-insensitive. The path is validated against the entity type at construction time — an invalid path throwsArgumentExceptionat startup. A null value at runtime throwsInvalidOperationExceptionwith a descriptive message. - For point reads and id-based deletes, the partition key value must be supplied explicitly via
GetByID(id, partitionKeyValue),FindByID(id, partitionKeyValue)andDeleteBy(id, partitionKeyValue). The single-argument overloads (GetByID(dynamic id), etc.) throwInvalidOperationExceptionwhen a partition key is configured — they are only valid for containers without a partition key. - Partition key properties of type
bool,int,long,float,doubleordecimalare mapped to the correct Cosmos DB native type —boolusesPartitionKey(bool), numeric types usePartitionKey(double). String properties always usePartitionKey(string). - If you override
DeleteItemInternalAsync(string id)orDeleteItemInternalAsync(string id, PartitionKey partitionKey)for custom deletion logic (audit, soft-delete, etc.), also overrideDeleteItemInternalAsync(TEntity entity, string id)—DeleteBy(TEntity)dispatches to the entity overload. Addreturnsdynamic(interface contract) — call.ToString()to get the id as a string.- Entities must expose a string property that resolves to the Cosmos DB document
id— either namedId,id, or decorated with[JsonPropertyName("id")]. If none is found,AddandDeleteBy(entity)throwInvalidOperationException.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Infrastructure.Data.Abstractions (>= 8.0.0)
- Microsoft.Azure.Cosmos (>= 3.61.0)
- Microsoft.Extensions.Options (>= 10.0.8)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Adds GetByID(id, partitionKeyValue), FindByID(id, partitionKeyValue) and DeleteBy(id, partitionKeyValue) overloads for correct point-reads and deletes on partitioned containers. Single-argument overloads now throw when a partition key is configured. Non-string partition key properties (bool, int, long, double) are now routed to the correct PartitionKey constructor type.