Complete Azure Cosmos DB library with enterprise features for .NET 9 including repository pattern, caching, bulk operations, validation, and comprehensive audit trails
$ dotnet add package CosmoBase
CosmoBase – Enterprise-grade Azure Cosmos DB library with advanced caching, validation, bulk operations, intelligent soft-delete handling, and comprehensive audit field management.
Stop reinventing the Cosmos DB wheel. Every project ends up building the same patterns: audit fields, validation, bulk operations, caching, retry logic. Then you copy-paste between projects, and half your implementations fall behind while the other half get new features.
CosmoBase centralizes all the boilerplate so you can focus on your business logic instead of low-level infrastructure concerns. One library, battle-tested patterns, automatic updates for all your projects.
Raw Cosmos SDK:
// Manual audit fields, custom retry logic, bulk operation error handling...
var response = await container.CreateItemAsync(item);
item.CreatedOnUtc = DateTime.UtcNow;
item.CreatedBy = GetCurrentUser(); // Hope this works
// 50+ lines of boilerplate per operation
With CosmoBase:
// Audit fields, retries, validation, bulk operations - all handled
await _writer.CreateAsync(product);
Enterprise-ready from day one with features you'll eventually need: soft deletes, multi-region routing, comprehensive caching, and bulletproof bulk operations.
IAsyncEnumerable<T>CreatedOnUtc, UpdatedOnUtc, CreatedBy, UpdatedBy field managementincludeDeleted parametersFrom the command line:
dotnet add package CosmoBase
Or via NuGet Package Manager in Visual Studio:
Install-Package CosmoBase
In appsettings.json:
{
"CosmoBase": {
"CosmosClientConfigurations": [
{
"Name": "Primary",
"ConnectionString": "AccountEndpoint=https://myaccount.documents.azure.com:443/;AccountKey=mykey==;",
"NumberOfWorkers": 10,
"AllowBulkExecution": true,
"ConnectionMode": "Direct",
"MaxRetryAttempts": 5,
"MaxRetryWaitTimeInSeconds": 30
},
{
"Name": "ReadReplica",
"ConnectionString": "AccountEndpoint=https://myaccount-eastus.documents.azure.com:443/;AccountKey=mykey2==;",
"NumberOfWorkers": 5,
"AllowBulkExecution": false,
"ConnectionMode": "Direct",
"MaxRetryAttempts": 3,
"MaxRetryWaitTimeInSeconds": 15
}
],
"CosmosModelConfigurations": [
{
"ModelName": "ProductDao",
"DatabaseName": "ProductCatalog",
"CollectionName": "Products",
"PartitionKey": "Category",
"ReadCosmosClientConfigurationName": "ReadReplica",
"WriteCosmosClientConfigurationName": "Primary"
},
{
"ModelName": "OrderDao",
"DatabaseName": "OrderManagement",
"CollectionName": "Orders",
"PartitionKey": "CustomerId",
"ReadCosmosClientConfigurationName": "Primary",
"WriteCosmosClientConfigurationName": "Primary"
}
]
}
}
⚠️ Common Configuration Pitfalls:
- ModelName: Must match your DAO class name exactly (e.g.,
"ProductDao", not"Product")- PartitionKey: Must be the property name from your DAO class (e.g.,
"Category", not"/category")These are the most common configuration mistakes that cause runtime errors!
CosmoBase requires a user context for audit field tracking. Choose the approach that fits your application:
using CosmoBase.DependencyInjection;
using CosmoBase.Abstractions.Interfaces;
var builder = WebApplication.CreateBuilder(args);
// Custom user context that reads from HTTP context
public class WebUserContext : IUserContext
{
private readonly IHttpContextAccessor _httpContextAccessor;
public WebUserContext(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public string? GetCurrentUser()
{
var context = _httpContextAccessor.HttpContext;
return context?.User?.Identity?.Name
?? context?.User?.FindFirst("sub")?.Value
?? "Anonymous";
}
}
// Register HTTP context accessor and custom user context
builder.Services.AddHttpContextAccessor();
builder.Services.AddSingleton<IUserContext, WebUserContext>();
// Register CosmoBase with user context
builder.Services.AddCosmoBase(
builder.Configuration,
builder.Services.BuildServiceProvider().GetRequiredService<IUserContext>(),
config =>
{
// Optional: Override specific settings
config.CosmosClientConfigurations
.First(c => c.Name == "Primary")
.NumberOfWorkers = 12;
});
> **⚠️ Important Configuration Note:**
> CosmoBase automatically configures System.Text.Json serialization for proper `[JsonPropertyName]` attribute handling. This ensures your DAO's `[JsonPropertyName("id")]` attribute works correctly with Cosmos DB's lowercase "id" requirement.
var app = builder.Build();
var builder = Host.CreateApplicationBuilder(args);
// Use system user context for background services
builder.Services.AddCosmoBaseWithSystemUser(
builder.Configuration,
"DataProcessor"); // System user name
var host = builder.Build();
var builder = WebApplication.CreateBuilder(args);
// Use delegate for custom user resolution logic
builder.Services.AddCosmoBaseWithUserProvider(
builder.Configuration,
() =>
{
// Your custom logic to resolve current user
return GetCurrentUserFromJwt() ?? "System";
});
var app = builder.Build();
High-level data services provide the best developer experience with automatic audit field management:
public class ProductService
{
private readonly ICosmosDataReadService<Product, ProductDao> _reader;
private readonly ICosmosDataWriteService<Product, ProductDao> _writer;
public ProductService(
ICosmosDataReadService<Product, ProductDao> reader,
ICosmosDataWriteService<Product, ProductDao> writer)
{
_reader = reader;
_writer = writer;
}
public async Task ProcessProductsAsync()
{
// Create with automatic validation, retry, and audit fields
var newProduct = new Product { Id = "123", Name = "Widget" };
await _writer.CreateAsync(newProduct);
// CreatedOnUtc, UpdatedOnUtc, CreatedBy, UpdatedBy automatically set
// Stream products with intelligent caching
await foreach (var product in _reader.GetAllAsync(
limit: 100, offset: 0, count: 500))
{
await ProcessProduct(product);
}
// Get cached count (15-minute cache)
var totalCount = await _reader.GetCountWithCacheAsync("electronics", 15);
}
}
For advanced scenarios requiring more control:
public class AdvancedProductService
{
private readonly ICosmosRepository<ProductDao> _repository;
public AdvancedProductService(ICosmosRepository<ProductDao> repository)
{
_repository = repository;
}
public async Task AdvancedOperationsAsync()
{
// Get item with soft-delete control
var product = await _repository.GetItemAsync(
"product123",
"electronics",
includeDeleted: false);
// Intelligent cached count with custom expiry
var count = await _repository.GetCountWithCacheAsync(
"electronics",
cacheExpiryMinutes: 30);
// Query with array property filtering
var premiumProducts = await _repository.GetAllByArrayPropertyAsync(
"tags",
"category",
"premium",
includeDeleted: false);
// Create with automatic audit fields
var newProduct = new ProductDao
{
Id = "new-product"
};
await _repository.CreateItemAsync(newProduct);
// CreatedOnUtc, UpdatedOnUtc, CreatedBy, UpdatedBy automatically populated
// Bulk operations with detailed error handling and audit fields
try
{
await _repository.BulkUpsertAsync(
products,
"electronics",
batchSize: 50,
maxConcurrency: 10);
}
catch (CosmoBaseException ex) when (ex.Data.Contains("BulkUpsertResult"))
{
var result = (BulkExecuteResult<ProductDao>)ex.Data["BulkUpsertResult"]!;
HandlePartialFailure(result);
}
// Custom LINQ queries
var expensiveProducts = _repository.Queryable
.Where(p => p.Price > 1000 && !p.Deleted)
.ToAsyncEnumerable();
}
}
CosmoBase automatically manages audit fields across all operations:
// All CRUD operations automatically set audit fields
var product = new ProductDao { Id = "123", Name = "Widget" };
// Create operation sets all fields
await repository.CreateItemAsync(product);
// Result: CreatedOnUtc, UpdatedOnUtc, CreatedBy, UpdatedBy all populated
// Update operation sets modified fields only
product.Name = "Updated Widget";
await repository.ReplaceItemAsync(product);
// Result: UpdatedOnUtc and UpdatedBy updated, CreatedOnUtc/CreatedBy preserved
// Upsert operation intelligently determines create vs update
await repository.UpsertItemAsync(product);
// Result: Automatically handles create vs update audit field logic
// Bulk operations handle audit fields for all items
await repository.BulkInsertAsync(products, "partition");
// Result: All items get proper audit fields based on operation type
Choose the user context approach that fits your application:
// 1. System user for background services
services.AddCosmoBaseWithSystemUser(configuration, "BackgroundService");
// 2. Delegate function for custom logic
services.AddCosmoBaseWithUserProvider(configuration, () =>
{
return HttpContext.Current?.User?.Identity?.Name ?? "Anonymous";
});
// 3. Custom implementation for complex scenarios
public class JwtUserContext : IUserContext
{
public string? GetCurrentUser()
{
// Extract user from JWT, database, etc.
return ExtractUserFromToken();
}
}
services.AddCosmoBase(configuration, new JwtUserContext());
// 4. Different contexts for different scenarios
#if DEBUG
services.AddCosmoBase(configuration, new SystemUserContext("Development"));
#else
services.AddCosmoBase(configuration, new ProductionUserContext());
#endif
Built-in count caching with age-based invalidation:
// Cache for 15 minutes, auto-invalidated on mutations
var count = await repository.GetCountWithCacheAsync("partition", 15);
// Force fresh count (bypass cache)
var freshCount = await repository.GetCountWithCacheAsync("partition", 0);
// Manual cache invalidation (automatic after creates/deletes)
repository.InvalidateCountCache("partition");
Extensible validation system with detailed error reporting:
// Custom validator example
public class ProductValidator : CosmosValidator<ProductDao>
{
public override void ValidateDocument(ProductDao item, string operation, string partitionKeyProperty)
{
base.ValidateDocument(item, operation, partitionKeyProperty);
// Custom business rules
if (string.IsNullOrEmpty(item.Name))
throw new ArgumentException("Product name is required");
if (item.Price <= 0)
throw new ArgumentException("Product price must be positive");
}
}
// Register custom validator
services.AddSingleton<ICosmosValidator<ProductDao>, ProductValidator>();
Consistent soft-delete handling across all operations:
// Get active items only (default)
var activeProducts = await repository.GetAllByArrayPropertyAsync(
"categories", "type", "electronics");
// Include soft-deleted items
var allProducts = await repository.GetAllByArrayPropertyAsync(
"categories", "type", "electronics", includeDeleted: true);
// Soft delete vs hard delete
await repository.DeleteItemAsync("id", "partition", DeleteOptions.SoftDelete);
await repository.DeleteItemAsync("id", "partition", DeleteOptions.HardDelete);
High-performance bulk operations with comprehensive error reporting:
try
{
await repository.BulkInsertAsync(documents, "partition");
}
catch (CosmoBaseException ex) when (ex.Data.Contains("BulkInsertResult"))
{
var result = (BulkExecuteResult<DocumentDao>)ex.Data["BulkInsertResult"]!;
Console.WriteLine($"Success rate: {result.SuccessRate:F1}%");
Console.WriteLine($"Total RUs consumed: {result.TotalRequestUnits}");
// Retry failed items that are retryable
var retryableItems = result.FailedItems
.Where(f => f.IsRetryable)
.Select(f => f.Item);
if (retryableItems.Any())
{
await repository.BulkInsertAsync(retryableItems, "partition");
}
}
CosmoBase uses a layered approach with automatic JSON-based mapping:
// DTO - exposed to your application
public class Product
{
public string Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
// DAO - stored in Cosmos DB with audit fields
public class ProductDao : ICosmosDataModel
{
[JsonPropertyName("id")]
public string Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
// Audit fields automatically managed
public DateTime? CreatedOnUtc { get; set; }
public DateTime? UpdatedOnUtc { get; set; }
public string? CreatedBy { get; set; }
public string? UpdatedBy { get; set; }
public bool Deleted { get; set; }
}
// Services handle mapping automatically
var dataService = serviceProvider.GetService<ICosmosDataWriteService<Product, ProductDao>>();
await dataService.CreateAsync(new Product { Id = "123", Name = "Widget" });
Built-in telemetry for monitoring and performance optimization:
// Metrics automatically tracked:
// - cosmos.request_charge (RU consumption)
// - cosmos.retry_count (Retry attempts)
// - cosmos.cache_hit_count (Cache effectiveness)
// - cosmos.cache_miss_count (Cache misses)
// Access via standard .NET metrics APIs
// Compatible with OpenTelemetry, Prometheus, Azure Monitor
| Property | Type | Description | Default |
|---|---|---|---|
Name | string | Unique name for this client configuration | Required |
ConnectionString | string | Cosmos DB connection string | Required |
NumberOfWorkers | int | Degree of parallelism for bulk operations (1-100) | Required |
AllowBulkExecution | bool? | Enable bulk operations for better throughput | true |
ConnectionMode | string? | Connection mode: "Direct" or "Gateway". Direct is faster, Gateway works better through firewalls | "Direct" |
MaxRetryAttempts | int? | Maximum retry attempts for rate-limited requests (0-20) | 9 |
MaxRetryWaitTimeInSeconds | int? | Maximum wait time in seconds for retries (1-300) | 30 |
| Property | Description |
|---|---|
ModelName | CRITICAL: Must match your DAO class name exactly (e.g., "ProductDao", not "Product") |
DatabaseName | Name of the Cosmos DB database |
CollectionName | Name of the container/collection |
PartitionKey | CRITICAL: Must be the property name from your DAO class (e.g., "Category", not "/category") |
ReadCosmosClientConfigurationName | Name of the client to use for read operations |
WriteCosmosClientConfigurationName | Name of the client to use for write operations |
BadRequest (400) errors:
ModelName matches your DAO class name exactly (e.g., "ProductDao", not "Product")PartitionKey is the property name from your DAO class (e.g., "Category", not "/category")ICosmosDataModel with [JsonPropertyName("id")] on the Id propertyAudit field issues:
IUserContext) is properly registered and returns valid user identifiersSerialization issues:
[JsonPropertyName("id")] on your DAO's Id property (required by Cosmos DB)PropertyNamingPolicy = JsonNamingPolicy.CamelCase as it conflicts with partition key casingService registration issues:
ICosmosDataWriteService<TDto, TDao>AddCosmoBase()SystemUserContext for background services to avoid HTTP context overheadIAsyncEnumerable<T>CosmoBase follows semantic versioning. Breaking changes will be clearly documented and migration guides provided for major version updates.
No breaking changes have been introduced since the initial 0.1.0 release. All updates have been focused on:
When breaking changes are necessary (major version bumps), this section will provide:
This project is licensed under the Apache License.
Contributions, issues, and feature requests are welcome! Please open an issue or submit a pull request.
dotnet testMade with ❤️ and 🚀 by Achilleas Tziazas