Purpose
- Contains helpers for loading and managing Cosmos DB connection strings and configuration providers.
Quick start
dotnet build SkyCMS.slnWhere to look
- Providers and configuration classes:
Cosmos.ConnectionStrings/*. - Single-tenant vs multi-tenant configuration implementations.
Notes
- Changing connection string handling affects all Cosmos consumers; coordinate changes and add tests.
Note: This project is located in the
Cosmos.ConnectionStrings/folder but the project file isCosmos.DynamicConfig.csproj. Both names refer to the same package - the namespace and assembly name isCosmos.DynamicConfig.
Cosmos.DynamicConfig is a configuration management system for multi-tenant applications. It provides dynamic, domain-based configuration and connection string resolution so a single application instance can serve multiple tenants with different database and storage settings based on the incoming request's domain name.
- Domain-Based Routing: Automatically resolves configuration based on request domain
- Dynamic Connection Strings: Per-tenant database and storage connection strings
- Runtime Configuration: Configuration values resolved at runtime without application restart
- Tenant Isolation: Complete separation of tenant data and resources
- Database Connections: Per-tenant database connection string management
- Storage Connections: Per-tenant blob storage connection string management
- Custom Configuration: Key-value configuration pairs per tenant
- Memory Caching: Efficient caching of configuration data for performance (10-second TTL)
- Domain Validation: Validate domain names against configured tenants via
ValidateDomainName() - Metrics Collection: Built-in metrics tracking for tenant resource usage
- Entity Framework Integration: Works with Cosmos DB, SQL Server, and MySQL via FlexDb provider detection
- HTTP Context Integration: Seamless integration with ASP.NET Core pipeline
Defines the contract for dynamic configuration services, providing methods for retrieving connection strings and configuration values based on domain context.
Properties:
IsMultiTenantConfigured: Returnstrueif any tenant connection is configured
Methods:
GetDatabaseConnectionString(domainName): Retrieves tenant database connectionGetStorageConnectionString(domainName): Retrieves tenant storage connectionGetConfigurationValue(key): Gets standard configuration valuesGetConnectionStringByName(name): Gets named connection strings
The main implementation that handles configuration resolution, caching, and database interactions for multi-tenant scenarios.
Key Features:
- Requires
IHttpContextAccessor- throwsArgumentNullExceptionif not available - Validates
ConfigDbConnectionStringon construction - Caches tenant connections for 10 seconds using
IMemoryCache - Uses FlexDb for automatic database provider detection
Important Notes:
- The constructor requires an active HTTP context. If your code runs outside an HTTP request (e.g., background services), you must pass the domain explicitly to the API methods.
- If
HttpContextis null during construction, anArgumentNullExceptionis thrown. - Configuration database connection string is mandatory and validated on startup.
Entity Framework DbContext for managing configuration data, connections, and metrics. The backing database is determined by your ConfigDbConnectionString (Cosmos DB, SQL Server, or MySQL).
Containers/Tables:
Connections: Tenant configuration entities (container: "config")Metrics: Usage metrics entities (container: "Metrics")
Represents a tenant configuration with domain mappings, connection strings, and metadata.
Properties:
Id: Unique identifier (Guid)DomainNames: Array of domain names for tenant routingDbConn: Tenant database connection stringStorageConn: Tenant storage connection stringCustomer: Owner/tenant nameResourceGroup: Azure resource groupPublisherMode: Publishing mode (Static, Decoupled, Headless, Hybrid, Static-dynamic)WebsiteUrl: Primary website URLOwnerEmail: Owner email address
Tracks resource usage per tenant.
Properties:
Id: Unique identifier (Guid)ConnectionId: Associated tenant connection IDTimeStamp: Metric timestamp (DateTimeOffset)BlobStorageBytes: Total blob storage usage (double?)BlobStorageEgressBytes: Outbound bandwidth (double?)BlobStorageIngressBytes: Inbound bandwidth (double?)BlobStorageTransactions: Storage transaction count (double?)DatabaseDataUsageBytes: Database data usage (double?)DatabaseIndexUsageBytes: Database index usage (double?)DatabaseRuUsage: Request Units consumed (double?)FrontDoorRequestBytes: Front Door request bytes (long?)FrontDoorResponseBytes: Front Door response bytes (long?)
ASP.NET Core middleware that captures the current request's domain and stores it in HttpContext.Items["Domain"] for downstream use.
This package is part of the SkyCMS solution. You can obtain it by cloning the SkyCMS GitHub repository.
Configure the main configuration database connection string (required key: ConnectionStrings:ConfigDbConnectionString).
{
"ConnectionStrings": {
"ConfigDbConnectionString": "AccountEndpoint=https://your-cosmos-account.documents.azure.com:443/;AccountKey=your-key;Database=your-config-db"
}
}Alternative examples:
SQL Server:
{
"ConnectionStrings": {
"ConfigDbConnectionString": "Server=tcp:your-sql.database.windows.net,1433;Initial Catalog=YourConfigDb;User ID=youruser;Password=yourpassword;"
}
}MySQL:
{
"ConnectionStrings": {
"ConfigDbConnectionString": "Server=your-mysql;Port=3306;uid=youruser;pwd=yourpassword;database=YourConfigDb;"
}
}Each tenant is configured with a Connection entity that includes:
- Domain Names: Array of domain names for the tenant
- Database Connection: Tenant-specific database connection string
- Storage Connection: Tenant-specific storage connection string
- Publisher Mode: Website publishing mode (Static, Decoupled, Headless, etc.)
- Website URL: Primary website URL
- Customer Information: Owner name, email, and resource group
In your Program.cs or Startup.cs:
using Cosmos.DynamicConfig;
// Register HTTP context accessor
builder.Services.AddHttpContextAccessor();
// Register memory cache
builder.Services.AddMemoryCache();
// Register dynamic configuration provider
builder.Services.AddScoped<IDynamicConfigurationProvider, DynamicConfigurationProvider>();
// Add domain middleware to capture request domain
app.UseMiddleware<DomainMiddleware>();
Note: The provider requires an active HTTP request context to infer the tenant domain. For background services or out-of-band operations, pass the domain explicitly to the API methods (see below).using Cosmos.DynamicConfig;
public class TenantService
{
private readonly IDynamicConfigurationProvider _configProvider;
public TenantService(IDynamicConfigurationProvider configProvider)
{
_configProvider = configProvider;
}
public async Task<string> GetTenantData()
{
// Get current tenant's database connection
var dbConnection = _configProvider.GetDatabaseConnectionString();
// Get current tenant's storage connection
var storageConnection = _configProvider.GetStorageConnectionString();
// Get custom configuration value
var customSetting = _configProvider.GetConfigurationValue("CustomSetting");
return $"DB: {dbConnection}, Storage: {storageConnection}";
}
}Explicit domain usage (no HTTP context required):
var dbForFoo = _configProvider.GetDatabaseConnectionString("www.foo.com");
var storageForFoo = _configProvider.GetStorageConnectionString("www.foo.com");For scenarios where HTTP context is unavailable (background jobs, console apps, unit tests), explicitly pass the domain parameter:
Background Service Example:
public class TenantBackgroundService : BackgroundService
{
private readonly IDynamicConfigurationProvider _configProvider;
private readonly ILogger<TenantBackgroundService> _logger;
public TenantBackgroundService(
IDynamicConfigurationProvider configProvider,
ILogger<TenantBackgroundService> logger)
{
_configProvider = configProvider;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var tenantDomains = new[] { "tenant1.example.com", "tenant2.example.com" };
foreach (var domain in tenantDomains)
{
// Pass domain explicitly - no HTTP context needed
var dbConnection = _configProvider.GetDatabaseConnectionString(domain);
var storageConnection = _configProvider.GetStorageConnectionString(domain);
_logger.LogInformation("Processing tenant: {Domain}", domain);
// Process tenant data...
await ProcessTenantDataAsync(domain, dbConnection, storageConnection);
}
}
}Console Application Example:
class Program
{
static async Task Main(string[] args)
{
var services = new ServiceCollection();
// Configure services without HTTP context
services.AddDbContext<DynamicConfigDbContext>(options =>
options.UseCosmos(configConnectionString, "ConfigDb"));
services.AddMemoryCache();
services.AddSingleton<IDynamicConfigurationProvider, DynamicConfigurationProvider>();
var provider = services.BuildServiceProvider();
var configProvider = provider.GetRequiredService<IDynamicConfigurationProvider>();
// Explicitly specify domain
var domain = "client.example.com";
var dbConn = configProvider.GetDatabaseConnectionString(domain);
Console.WriteLine($"Database connection for {domain}: {dbConn}");
}
}Unit Test Example:
[TestMethod]
public void GetDatabaseConnectionString_WithExplicitDomain_ReturnsCorrectConnection()
{
// Arrange
var mockCache = new MemoryCache(new MemoryCacheOptions());
var configProvider = new DynamicConfigurationProvider(
configuration: mockConfig,
httpContextAccessor: null, // No HTTP context
memoryCache: mockCache,
logger: mockLogger);
// Act - Pass domain explicitly
var result = configProvider.GetDatabaseConnectionString("test.example.com");
// Assert
Assert.IsNotNull(result);
Assert.IsTrue(result.Contains("Database=TestDb"));
}API Methods Supporting Explicit Domain:
// All methods accept optional domain parameter
string GetDatabaseConnectionString(string? domain = null);
string GetStorageConnectionString(string? domain = null);
string GetConfigurationValue(string key, string? domain = null);
Task<bool> ValidateDomainName(string domain);
Task<Connection?> GetConnectionByDomainAsync(string domain);When to Use Explicit Domain:
- Background services / hosted services
- Console applications
- Unit tests
- Scheduled jobs (Hangfire, Quartz)
- Azure Functions (when not triggered by HTTP)
- Regular HTTP requests (use middleware - automatic)
The Metrics entity tracks resource consumption per tenant for billing, monitoring, and capacity planning.
Recording Metrics:
public class MetricsService
{
private readonly DynamicConfigDbContext _dbContext;
public async Task RecordTenantMetricsAsync(
Guid connectionId,
double blobStorageBytes,
double blobTransactions,
double databaseRUs)
{
var metric = new Metric
{
Id = Guid.NewGuid(),
ConnectionId = connectionId,
TimeStamp = DateTimeOffset.UtcNow,
BlobStorageBytes = blobStorageBytes,
BlobStorageTransactions = blobTransactions,
DatabaseRequestUnits = databaseRUs
};
_dbContext.Metrics.Add(metric);
await _dbContext.SaveChangesAsync();
}
}Querying Metrics:
public async Task<double> GetMonthlyStorageUsageAsync(Guid connectionId)
{
var startOfMonth = new DateTimeOffset(
DateTime.UtcNow.Year,
DateTime.UtcNow.Month,
1, 0, 0, 0, TimeSpan.Zero);
var totalBytes = await _dbContext.Metrics
.Where(m => m.ConnectionId == connectionId && m.TimeStamp >= startOfMonth)
.SumAsync(m => m.BlobStorageBytes ?? 0);
return totalBytes;
}Automated Metrics Collection:
public class MetricsCollectorService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// Collect metrics for all tenants every hour
await CollectAllTenantMetricsAsync();
await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
}
}
private async Task CollectAllTenantMetricsAsync()
{
var connections = await _dbContext.Connections.ToListAsync();
foreach (var connection in connections)
{
// Query Azure/AWS APIs for actual usage
var metrics = await FetchTenantResourceMetricsAsync(connection);
await RecordTenantMetricsAsync(connection.Id, metrics);
}
}
}public class ConfigurationController : ControllerBase
{
private readonly IDynamicConfigurationProvider _configProvider;
public ConfigurationController(IDynamicConfigurationProvider configProvider)
{
_configProvider = configProvider;
}
[HttpGet("tenant-info")]
public async Task<IActionResult> GetTenantInfo()
{
// Check if multi-tenant is configured (any tenant connection present)
if (!_configProvider.IsMultiTenantConfigured)
{
return BadRequest("Multi-tenant not configured");
}
// Get configuration for current domain
var dbConnection = _configProvider.GetDatabaseConnectionString();
var storageConnection = _configProvider.GetStorageConnectionString();
return Ok(new
{
DatabaseConfigured = !string.IsNullOrEmpty(dbConnection),
StorageConfigured = !string.IsNullOrEmpty(storageConnection)
});
}
[HttpGet("validate-domain/{domain}")]
public async Task<IActionResult> ValidateDomain(string domain)
{
var isValid = await _configProvider.ValidateDomainName(domain);
return Ok(new { IsValid = isValid });
}
}public class TenantManagementService
{
private readonly IDynamicConfigurationProvider _configProvider;
public TenantManagementService(IDynamicConfigurationProvider configProvider)
{
_configProvider = configProvider;
}
public async Task<Connection> CreateTenant(TenantRequest request)
{
var connection = new Connection
{
DomainNames = request.Domains,
DbConn = request.DatabaseConnectionString,
StorageConn = request.StorageConnectionString,
Customer = request.CustomerName,
ResourceGroup = request.ResourceGroup,
PublisherMode = request.PublisherMode,
WebsiteUrl = request.WebsiteUrl,
OwnerEmail = request.OwnerEmail
};
// Save to configuration database
using var context = new DynamicConfigDbContext(options);
context.Connections.Add(connection);
await context.SaveChangesAsync();
return connection;
}
}public class MetricsService
{
private readonly DynamicConfigDbContext _context;
public MetricsService(DynamicConfigDbContext context)
{
_context = context;
}
public async Task RecordUsageMetrics(Guid connectionId, UsageData usage)
{
var metric = new Metric
{
ConnectionId = connectionId,
TimeStamp = DateTimeOffset.UtcNow,
BlobStorageBytes = usage.StorageBytes,
BlobStorageEgressBytes = usage.EgressBytes,
BlobStorageIngressBytes = usage.IngressBytes,
BlobStorageTransactions = usage.Transactions,
DatabaseDataUsageBytes = usage.DatabaseBytes,
DatabaseRuUsage = usage.RequestUnits,
FrontDoorRequestBytes = usage.RequestBytes,
FrontDoorResponseBytes = usage.ResponseBytes
};
_context.Metrics.Add(metric);
await _context.SaveChangesAsync();
}
}| Method | Description | Parameters |
|---|---|---|
GetDatabaseConnectionString |
Get tenant database connection | domainName: string (optional) |
GetStorageConnectionString |
Get tenant storage connection | domainName: string (optional) |
GetConfigurationValue |
Get configuration value by key | key: string |
GetConnectionStringByName |
Get connection string by name | name: string |
ValidateDomainName |
Validate if domain is configured | domainName: string |
| Property | Type | Description |
|---|---|---|
Id |
Guid |
Unique connection identifier |
DomainNames |
string[] |
Array of associated domain names |
DbConn |
string |
Database connection string |
StorageConn |
string |
Storage connection string |
Customer |
string |
Customer/tenant name |
ResourceGroup |
string |
Azure resource group |
PublisherMode |
string |
Publishing mode |
WebsiteUrl |
string |
Primary website URL |
OwnerEmail |
string |
Owner email address |
| Property | Type | Description |
|---|---|---|
Id |
Guid |
Unique metric identifier |
ConnectionId |
Guid |
Associated connection ID |
TimeStamp |
DateTimeOffset |
Metric timestamp |
BlobStorageBytes |
double? |
Blob storage usage in bytes |
BlobStorageEgressBytes |
double? |
Outbound bandwidth usage |
BlobStorageIngressBytes |
double? |
Inbound bandwidth usage |
BlobStorageTransactions |
double? |
Storage transaction count |
DatabaseDataUsageBytes |
double? |
Database data usage |
DatabaseIndexUsageBytes |
double? |
Database index usage |
DatabaseRuUsage |
double? |
Request Units consumed |
The system supports various publishing modes for different tenant requirements:
- Static: Static website hosting
- Decoupled: Decoupled CMS architecture
- Headless: Headless CMS mode
- Hybrid: Hybrid static/dynamic content
- Static-dynamic: Mixed static and dynamic content
- .NET 9.0: Modern .NET framework
- Microsoft.EntityFrameworkCore: Entity Framework Core
- Microsoft.EntityFrameworkCore.Cosmos: Cosmos DB provider
- Microsoft.AspNetCore.Http.Abstractions: HTTP abstractions
- Microsoft.Extensions.Configuration: Configuration management
- Microsoft.Extensions.Caching.Memory: Memory caching
- AspNetCore.Identity.FlexDb: Flexible identity provider integration
- Resource Isolation: Complete separation of tenant data and resources
- Scalability: Support for unlimited number of tenants
- Cost Efficiency: Shared application infrastructure with isolated data
- Customization: Per-tenant configuration and feature flags
- Security: Tenant isolation and secure configuration management
- Memory Caching: Configuration data is cached for 10 seconds to reduce database calls
- Efficient Queries: Optimized Entity Framework queries for configuration lookup
- Connection Pooling: Efficient database connection management
- Lazy Loading: Configuration loaded only when needed
- Domain Validation: Prevents unauthorized domain access
- Connection String Security: Secure storage of sensitive connection information
- Tenant Isolation: Complete separation of tenant configurations
- HTTP Context Integration: Secure domain resolution from request context
The system includes comprehensive metrics collection for:
- Storage Usage: Blob storage bytes, ingress/egress bandwidth
- Database Usage: Data and index usage, Request Units consumption
- Transaction Tracking: Storage and database transaction counts
- Network Traffic: Front Door request/response bytes
Licensed under the GNU Public License, Version 3.0. See the LICENSE file for details.
This project is part of the SkyCMS ecosystem. For contribution guidelines and more information, visit the SkyCMS GitHub repository.
- HTTP context is not available
- Ensure
AddHttpContextAccessor()is registered and the code runs within an HTTP request. For background jobs, pass a domain string to the API.
- Ensure
- Missing
ConfigDbConnectionString- The provider requires
ConnectionStrings:ConfigDbConnectionString. Add it to configuration or environment variables.
- The provider requires
- Domain not found
ValidateDomainNamereturns false if the domain is not configured. Add the domain to aConnectionentity in the configuration database.
- Stale configuration
- Results are cached for 10 seconds. Wait for cache expiry or adjust code to refresh if you just updated tenant settings.
- Editor Documentation - Content management interface using dynamic configuration
- Publisher Documentation - Public website using multi-tenant support
- Database Configuration Guide - Database provider setup instructions
- Storage Configuration Guide - Storage provider setup instructions
- Azure Installation Guide - Azure deployment and configuration
- appsettings.json - Application configuration examples
- docker-compose.yml - Docker container configuration