Configuration System Enhancement Guide
February 23, 2026 · View on GitHub
Overview
This document describes the enhanced configuration system features introduced in Phase 2 of the ACAT modernization project. The enhancements include:
- JSON Schema Validation - Validate configurations against JSON schemas
- Configuration Hot-Reload - Automatically reload configurations when files change
- Environment-Specific Configuration - Support for different environments (Development, Testing, Staging, Production)
- Configuration Versioning - Track and migrate configuration versions
JSON Schema Validation
Overview
The JsonSchemaValidator class provides basic JSON schema validation to ensure configuration files conform to expected structures.
Usage
using ACAT.Core.Configuration;
using Microsoft.Extensions.Logging;
// Create validator
var validator = new JsonSchemaValidator(logger);
// Load schema
string schemaPath = "schemas/json/actuator-settings.schema.json";
validator.LoadSchema("actuator-settings", schemaPath);
// Validate configuration
string configPath = "config/ActuatorSettings.json";
bool isValid = validator.Validate("actuator-settings", configPath, out List<string> errors);
if (!isValid)
{
foreach (var error in errors)
{
Console.WriteLine($"Validation error: {error}");
}
}
Features
- Load and cache JSON schemas
- Validate JSON files against schemas
- Validate JSON content strings
- Detailed error reporting
- Type checking (string, number, boolean, array, object)
- Required property validation
Configuration Hot-Reload
Overview
The ConfigurationReloadService monitors configuration files for changes and notifies listeners when files are modified.
Usage
using ACAT.Core.Configuration;
using Microsoft.Extensions.Logging;
// Create reload service
var reloadService = new ConfigurationReloadService(logger);
// Subscribe to reload events
reloadService.ConfigurationReloaded += (sender, e) =>
{
logger.LogInformation("Configuration reloaded: {FilePath}", e.FilePath);
// Reload your configuration here
ReloadMyConfiguration(e.FilePath);
};
reloadService.ConfigurationReloadFailed += (sender, e) =>
{
logger.LogError("Configuration reload failed: {FilePath}. Error: {Error}",
e.FilePath, e.ErrorMessage);
};
// Start monitoring
string configPath = "config/settings.json";
reloadService.StartMonitoring(configPath);
// Stop monitoring when done
reloadService.StopMonitoring(configPath);
// Or stop all monitoring
reloadService.StopAll();
// Clean up
reloadService.Dispose();
Features
- File system monitoring with FileSystemWatcher
- Debouncing to prevent multiple reloads (500ms delay)
- Event-based notifications
- Support for multiple files
- Automatic cleanup on dispose
Integration with JsonConfigurationLoader
using ACAT.Core.Utility;
// Create loader with hot-reload enabled
var loader = new JsonConfigurationLoader<MyConfig>(
validator: myValidator,
logger: logger,
enableHotReload: true // Enable hot-reload
);
// Subscribe to reload events
loader.ConfigurationReloaded += (sender, e) =>
{
// Reload configuration
var newConfig = loader.Load(e.FilePath);
ApplyNewConfiguration(newConfig);
};
// Load and enable monitoring
var config = loader.Load("config/settings.json");
loader.EnableHotReload("config/settings.json");
// Disable when done
loader.DisableHotReload("config/settings.json");
loader.Dispose();
Environment-Specific Configuration
Overview
The EnvironmentConfiguration class supports loading different configuration files based on the environment (Development, Testing, Staging, Production).
Configuration Hierarchy
Configuration is loaded and merged in the following priority order (lowest to highest):
- Base configuration –
config.json— shared defaults for all environments - Environment-specific –
config.{Environment}.json— overrides for the active environment - Local override –
config.local.json— developer-specific overrides (gitignored) - Environment variables –
ACAT_<PropertyName>— runtime overrides
Use GetConfigurationFiles(baseFilePath) to retrieve the ordered list of existing files
that should be loaded and merged for the active environment.
Supported Environments
public enum ConfigurationEnvironment
{
Development,
Testing,
Staging,
Production
}
Environment Detection
The system detects the environment from environment variables in this order:
ACAT_ENVIRONMENTDOTNET_ENVIRONMENTASPNETCORE_ENVIRONMENT- Defaults to
Productionif none are set
Setting Environment
Option 1: Environment Variable
# Windows
set ACAT_ENVIRONMENT=Development
# Linux/Mac
export ACAT_ENVIRONMENT=Development
Option 2: Programmatic
var envConfig = new EnvironmentConfiguration(logger);
envConfig.SetEnvironment(ConfigurationEnvironment.Development);
File Naming Convention
Environment-specific files use the naming pattern: filename.{Environment}.extension
Examples:
- Base:
config.json - Development:
config.Development.json - Testing:
config.Testing.json - Staging:
config.Staging.json - Production:
config.Production.json - Local override:
config.local.json(gitignored — never commit this file)
Usage
using ACAT.Core.Configuration;
// Create environment configuration
var envConfig = new EnvironmentConfiguration(logger);
// Get environment-specific file path
string basePath = "config/settings.json";
string envPath = envConfig.GetEnvironmentFilePath(basePath);
// Returns: "config/settings.Development.json" if in Development and file exists
// "config/settings.json" otherwise
// Get the local override file path (e.g. for developer-specific settings)
string localPath = envConfig.GetLocalOverrideFilePath(basePath);
// Returns: "config/settings.local.json" (regardless of whether the file exists)
// Get all files in the configuration hierarchy (only existing files, lowest to highest priority)
IReadOnlyList<string> files = envConfig.GetConfigurationFiles(basePath);
// Returns e.g.: ["config/settings.json", "config/settings.Development.json", "config/settings.local.json"]
// Load configuration with environment overrides
var config = envConfig.LoadWithEnvironmentOverrides<MyConfig>(basePath);
Local Override Files
config.local.json files allow individual developers to maintain machine-specific settings
without affecting shared configuration or polluting source control.
Setup:
- Copy the relevant base file:
cp config.json config.local.json - Edit
config.local.jsonwith your local overrides - The file is automatically ignored by
.gitignore(*.local.json)
Important: Never commit *.local.json files to source control.
Environment Variable Overrides
You can override individual configuration properties using environment variables:
# Override property "Port" in configuration
set ACAT_PORT=9090
# Override property "Enabled"
set ACAT_ENABLED=true
The system will automatically apply these overrides when loading configuration.
Integration with JsonConfigurationLoader
// Create loader with environment support
var loader = new JsonConfigurationLoader<MyConfig>(
validator: myValidator,
logger: logger,
enableHotReload: false,
useEnvironmentConfig: true // Enable environment-specific config
);
// Load with environment support
var config = loader.LoadWithEnvironment("config/settings.json");
// Automatically loads "settings.Development.json" in Development environment
// And applies environment variable overrides
// Check current environment
var env = loader.GetCurrentEnvironment();
logger.LogInformation("Current environment: {Environment}", env);
Configuration Versioning
Overview
The ConfigurationVersionManager class tracks configuration file versions and supports migrations between versions.
Version Format
Configuration versions use semantic versioning: Major.Minor.Patch
Example: 1.2.3
- Major: Incompatible changes
- Minor: Backward-compatible features
- Patch: Backward-compatible bug fixes
Adding Version to Configuration
{
"version": "1.0.0",
"name": "My Configuration",
"settings": {
...
}
}
Alternative property name:
{
"configVersion": "1.0.0",
...
}
Usage
using ACAT.Core.Configuration;
// Create version manager
var versionManager = new ConfigurationVersionManager(logger);
// Set current version for configuration type
versionManager.SetCurrentVersion("actuator-settings",
new ConfigurationVersion(2, 0, 0));
// Check if file needs migration
string configPath = "config/ActuatorSettings.json";
if (versionManager.NeedsMigration("actuator-settings", configPath))
{
logger.LogInformation("Configuration needs migration");
// Migrate (creates backup automatically)
bool success = versionManager.MigrateConfiguration(
"actuator-settings",
configPath,
createBackup: true
);
}
// Get configuration version
var version = versionManager.GetConfigurationVersion(configPath);
logger.LogInformation("Configuration version: {Version}", version);
Creating Migration Handlers
Implement IConfigurationMigration interface:
public class ActuatorSettings_1_0_to_2_0 : IConfigurationMigration
{
public ConfigurationVersion FromVersion => new ConfigurationVersion(1, 0, 0);
public ConfigurationVersion ToVersion => new ConfigurationVersion(2, 0, 0);
public bool Migrate(JsonElement source, out JsonElement result, out string error)
{
try
{
// Perform migration logic
// Example: Rename property, add new fields, etc.
var dict = new Dictionary<string, object>();
// Copy existing properties
foreach (var prop in source.EnumerateObject())
{
if (prop.Name == "oldPropertyName")
{
// Rename property
dict["newPropertyName"] = prop.Value.ToString();
}
else
{
dict[prop.Name] = prop.Value.Clone();
}
}
// Add new required properties
dict["newField"] = "defaultValue";
dict["version"] = "2.0.0";
// Serialize back to JsonElement
string json = JsonSerializer.Serialize(dict);
result = JsonDocument.Parse(json).RootElement;
error = null;
return true;
}
catch (Exception ex)
{
result = default;
error = ex.Message;
return false;
}
}
}
// Register migration
var migration = new ActuatorSettings_1_0_to_2_0();
versionManager.RegisterMigration("actuator-settings", migration);
Complete Example
Comprehensive Configuration Loading
using ACAT.Core.Configuration;
using ACAT.Core.Utility;
using ACAT.Core.Validation;
using Microsoft.Extensions.Logging;
public class ConfigurationManager
{
private readonly ILogger _logger;
private readonly JsonConfigurationLoader<MyConfig> _loader;
private readonly JsonSchemaValidator _schemaValidator;
private readonly ConfigurationVersionManager _versionManager;
private MyConfig _currentConfig;
public ConfigurationManager(ILogger logger)
{
_logger = logger;
// Initialize schema validator
_schemaValidator = new JsonSchemaValidator(_logger);
_schemaValidator.LoadSchema("my-config", "schemas/json/my-config.schema.json");
// Initialize version manager
_versionManager = new ConfigurationVersionManager(_logger);
_versionManager.SetCurrentVersion("my-config", new ConfigurationVersion(2, 0, 0));
// Initialize loader with all features
_loader = new JsonConfigurationLoader<MyConfig>(
validator: new MyConfigValidator(),
logger: _logger,
enableHotReload: true,
useEnvironmentConfig: true
);
// Subscribe to reload events
_loader.ConfigurationReloaded += OnConfigurationReloaded;
}
public MyConfig LoadConfiguration(string basePath)
{
try
{
// Get environment-specific path
string configPath = GetConfigPath(basePath);
// Check if migration needed
if (_versionManager.NeedsMigration("my-config", configPath))
{
_logger.LogInformation("Migrating configuration...");
_versionManager.MigrateConfiguration("my-config", configPath, createBackup: true);
}
// Validate schema
if (!_schemaValidator.Validate("my-config", configPath, out List<string> errors))
{
_logger.LogError("Schema validation failed:");
foreach (var error in errors)
{
_logger.LogError(" - {Error}", error);
}
throw new Exception("Configuration schema validation failed");
}
// Load with environment overrides
_currentConfig = _loader.LoadWithEnvironment(basePath);
// Enable hot-reload
_loader.EnableHotReload(configPath);
_logger.LogInformation("Configuration loaded successfully from: {Path}", configPath);
return _currentConfig;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error loading configuration");
throw;
}
}
private void OnConfigurationReloaded(object sender, ConfigurationReloadEventArgs e)
{
_logger.LogInformation("Configuration file changed, reloading: {FilePath}", e.FilePath);
if (e.Success)
{
// Reload configuration
_currentConfig = _loader.Load(e.FilePath);
// Notify application
NotifyConfigurationChanged();
}
}
private string GetConfigPath(string basePath)
{
var envConfig = new EnvironmentConfiguration(_logger);
return envConfig.GetEnvironmentFilePath(basePath);
}
private void NotifyConfigurationChanged()
{
// Notify your application that configuration has changed
ConfigurationChanged?.Invoke(this, EventArgs.Empty);
}
public event EventHandler ConfigurationChanged;
}
Best Practices
1. Schema Validation
- Always validate configurations against schemas before use
- Keep schemas up-to-date with code changes
- Include schemas in version control
- Provide clear error messages for validation failures
2. Hot-Reload
- Use hot-reload only when appropriate (not all configs need it)
- Test reload behavior thoroughly
- Handle reload failures gracefully
- Consider thread-safety when reloading
3. Environment Configuration
- Use Production as the default environment
- Never include sensitive data in Development/Testing configs
- Document environment-specific differences
- Test all environments before deployment
4. Versioning
- Always include version in configuration files
- Increment version appropriately (major/minor/patch)
- Create migration handlers for breaking changes
- Always create backups before migration
- Test migrations thoroughly
5. Error Handling
- Always handle configuration load failures
- Provide meaningful error messages
- Log all configuration operations
- Have fallback/default configurations
6. Security
- Never log sensitive configuration values
- Secure environment variable access
- Validate all loaded configurations
- Use appropriate file permissions
Troubleshooting
Hot-Reload Not Working
Problem: Configuration changes not detected
Solutions:
- Ensure
enableHotReload=truewhen creating loader - Call
EnableHotReload(filePath)explicitly - Check file permissions
- Verify FileSystemWatcher limitations on your OS
- Check debounce delay (500ms default)
Environment Config Not Loading
Problem: Environment-specific file not loaded
Solutions:
- Verify environment variable is set correctly
- Check file naming convention (e.g.,
config.Development.json) - Ensure environment-specific file exists
- Set
useEnvironmentConfig=truewhen creating loader
Schema Validation Errors
Problem: Valid configuration fails schema validation
Solutions:
- Check schema is loaded correctly
- Verify JSON syntax
- Check required properties
- Validate property types
- Review schema definition
Migration Failures
Problem: Configuration migration fails
Solutions:
- Check migration handler logic
- Verify version numbers
- Review error messages
- Restore from backup if needed
- Test migration with copy first
API Reference
See inline XML documentation in source files:
JsonSchemaValidator.csConfigurationReloadService.csEnvironmentConfiguration.csConfigurationVersioning.csJsonConfigurationLoader.cs