Infrastructure.Data.CosmosDb
10.2.1
dotnet add package Infrastructure.Data.CosmosDb --version 10.2.1
NuGet\Install-Package Infrastructure.Data.CosmosDb -Version 10.2.1
<PackageReference Include="Infrastructure.Data.CosmosDb" Version="10.2.1" />
<PackageVersion Include="Infrastructure.Data.CosmosDb" Version="10.2.1" />
<PackageReference Include="Infrastructure.Data.CosmosDb" />
paket add Infrastructure.Data.CosmosDb --version 10.2.1
#r "nuget: Infrastructure.Data.CosmosDb, 10.2.1"
#:package Infrastructure.Data.CosmosDb@10.2.1
#addin nuget:?package=Infrastructure.Data.CosmosDb&version=10.2.1
#tool nuget:?package=Infrastructure.Data.CosmosDb&version=10.2.1
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 and also matches[JsonPropertyName]attributes. If the path cannot be resolved against the entity type (e.g. a pre-existing container whose path predates a model rename), operations fall back toPartitionKey.None(cross-partition). A null value at runtime for a resolvable path 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.
Container creation now reuses a pre-existing container regardless of its partition key path (no more ArgumentException on a mismatched path). Partition key path resolution falls back to PartitionKey.None when a path segment cannot be resolved against the entity, and matches [JsonPropertyName] before the C# property name.