Configuration in ConnectSoft Microservice Template¶
Purpose & Overview¶
Configuration is a fundamental aspect of any microservice, enabling applications to adapt to different environments, deployment contexts, and runtime requirements. In the ConnectSoft Microservice Template, configuration is managed through ASP.NET Core's extensible configuration model, enhanced with:
- Options Pattern: Strongly-typed, validated configuration classes
- Environment Awareness: Support for environment-specific overrides
- Fail-Fast Validation: Startup-time validation to prevent misconfigured deployments
- Azure App Configuration: Optional centralized configuration management
- Dynamic Refresh: Runtime configuration updates without redeployment
The template treats configuration as code—structured, versioned, validated, and testable.
Configuration Philosophy
Configuration is the foundation for microservice correctness. All settings are strongly typed, validated at startup, and managed through a consistent options pattern. This ensures services fail fast on misconfiguration rather than operating with undefined behavior.
Architecture Overview¶
Configuration Sources Hierarchy¶
Command-Line Arguments (Highest Priority)
↓
Environment Variables
↓
Azure App Configuration (if enabled)
↓
appsettings.{Environment}.json
↓
appsettings.json (Base)
↓
IConfiguration (Lowest Priority)
Configuration Flow¶
Configuration Sources
↓
IConfigurationBuilder (Program.cs)
├── appsettings.json
├── appsettings.{Environment}.json
├── Azure App Configuration (optional)
├── Environment Variables
└── Command-Line Args
↓
IConfiguration
↓
Options Pattern
├── Bind to POCOs
├── Validate with DataAnnotations
├── Validate with IValidateOptions<T>
└── Register with IOptions<T>
↓
Dependency Injection
└── Consumed by Services
Configuration Sources¶
appsettings.json (Base Configuration)¶
The base configuration file contains default values shared across all environments:
{
"Microservice": {
"MicroserviceName": "ConnectSoft.MicroserviceTemplate",
"StartupWarmupSeconds": 20
},
"Validation": {
"EnableClassLevelFailFast": true,
"EnableRuleLevelFailFast": true
}
}
Characteristics: - Loaded for all environments - Contains non-sensitive default values - Can be overridden by environment-specific files - Required file (not optional)
Environment-Specific Files¶
Environment-specific configuration files override base settings:
appsettings.Development.json: Local development overridesappsettings.Docker.json: Container-specific settingsappsettings.Production.json: Production overrides (if needed)
Example (appsettings.Development.json):
{
"Microservice": {
"MicroserviceName": "ConnectSoft.MicroserviceTemplate.Dev"
},
"Serilog": {
"MinimumLevel": {
"Default": "Debug"
}
}
}
Selection Logic:
The environment is determined by:
- ASPNETCORE_ENVIRONMENT environment variable
- DOTNET_ENVIRONMENT environment variable (alternative)
- Falls back to Production if not set
Environment Variables¶
Environment variables can override any configuration value:
# Override using double underscore notation
export Microservice__MicroserviceName="MyService"
export Microservice__StartupWarmupSeconds="30"
# Or using colon notation
export Microservice:MicroserviceName="MyService"
Use Cases: - Container deployments (Docker, Kubernetes) - CI/CD pipelines - Local development overrides - Secrets injection (via Key Vault references)
Command-Line Arguments¶
Command-line arguments provide the highest priority overrides:
Azure App Configuration (Optional)¶
Azure App Configuration provides centralized, dynamic configuration:
configurationBuilder.AddAzureAppConfiguration(options =>
{
options.Connect(connectionString)
.Select("ConnectSoft.MicroserviceTemplate:*", LabelFilter.Null)
.ConfigureRefresh(refresh =>
{
refresh.Register("ConnectSoft.MicroserviceTemplate:Settings:Sentinel", refreshAll: true);
refresh.SetRefreshInterval(TimeSpan.FromMinutes(30));
});
});
Features: - Centralized key-value storage - Dynamic refresh without redeployment - Environment labels for multi-environment support - Feature flags integration - Key Vault references for secrets
See Feature Flags for Azure App Configuration feature flags details.
Configuration Loading¶
Program.cs Configuration Setup¶
Configuration is loaded in DefineConfiguration:
// Program.cs
private static void DefineConfiguration(
string[] args,
HostBuilderContext hostBuilderContext,
IConfigurationBuilder configurationBuilder)
{
configurationBuilder.Sources.Clear();
configurationBuilder
.SetBasePath(hostBuilderContext.HostingEnvironment.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile(
$"appsettings.{hostBuilderContext.HostingEnvironment.EnvironmentName}.json",
optional: true,
reloadOnChange: true);
#if UseAzureAppConfigurationAsAdditionalConfigurationProvider
IConfigurationRoot configurationRoot = configurationBuilder.Build();
configurationBuilder.AddAzureAppConfiguration(options =>
{
string? connectionString = configurationRoot.GetConnectionString("AzureAppConfiguration");
options.Connect(connectionString)
.Select("ConnectSoft.MicroserviceTemplate:*", LabelFilter.Null)
.ConfigureRefresh(refresh =>
{
refresh.Register("ConnectSoft.MicroserviceTemplate:Settings:Sentinel", refreshAll: true);
refresh.SetRefreshInterval(TimeSpan.FromMinutes(30));
})
.ConfigureClientOptions(options =>
{
options.Retry.MaxRetries = 1;
});
#if UseAzureAppConfigurationAsFeatureFlagsProvider
options.UseFeatureFlags(featureFlagsOptions =>
{
featureFlagsOptions.Select("ConnectSoft.MicroserviceTemplate:*", LabelFilter.Null);
featureFlagsOptions.SetRefreshInterval(TimeSpan.FromMinutes(30));
});
#endif
});
#endif
configurationBuilder
.AddEnvironmentVariables()
.AddCommandLine(args);
}
Key Points:
- Sources.Clear(): Removes default providers to ensure explicit ordering
- reloadOnChange: true: Enables file-based configuration reloading
- Environment-specific files are optional
- Azure App Configuration is conditionally included
Configuration Precedence¶
Configuration sources are loaded in priority order (last wins):
| Priority | Source | Override Capability |
|---|---|---|
| Highest | Command-Line Arguments | Overrides everything |
| Environment Variables | Overrides files and Azure | |
| Azure App Configuration | Overrides file-based config | |
appsettings.{Environment}.json |
Overrides base file | |
| Lowest | appsettings.json |
Base defaults |
Options Pattern¶
Purpose¶
The Options Pattern provides: - Strong Typing: Configuration bound to POCO classes - Validation: Startup-time validation of required values - IntelliSense: IDE support for configuration properties - Refactoring Safety: Compile-time checking of configuration usage - Testability: Easy to mock and override in tests
Options Class Definition¶
Options classes are simple POCOs with DataAnnotations:
// MicroserviceOptions.cs
namespace ConnectSoft.MicroserviceTemplate.Options
{
using System.ComponentModel.DataAnnotations;
public sealed class MicroserviceOptions
{
public const string MicroserviceOptionsSectionName = "Microservice";
[Required]
required public string MicroserviceName { get; set; }
[Required]
[Range(0, int.MaxValue, ErrorMessage = "StartupWarmupSeconds must be 0 or positive.")]
required public int StartupWarmupSeconds { get; set; } = 20;
}
}
Characteristics:
- sealed: Prevents inheritance
- [Required]: Validates property is not null/empty
- [Range]: Validates numeric ranges
- Section name constant for consistency
- Default values where appropriate
Options Validator¶
Options are validated using source-generated validators:
// ValidateMicroserviceOptions.cs
namespace ConnectSoft.MicroserviceTemplate.Options
{
using Microsoft.Extensions.Options;
/// <summary>
/// Source-generated validator for MicroserviceOptions.
/// </summary>
[OptionsValidator]
public partial class ValidateMicroserviceOptions : IValidateOptions<MicroserviceOptions>
{
}
}
The [OptionsValidator] attribute instructs the Roslyn source generator to:
- Generate validation logic based on DataAnnotations
- Implement IValidateOptions<TOptions>
- Validate nested objects with [ValidateObjectMembers] (if needed)
Options Registration¶
Options are registered in OptionsExtensions.cs:
// OptionsExtensions.cs
private static TOptions AddCustomOptions<TOptions, TOptionsValidator>(
IServiceCollection services,
IConfiguration configuration,
string sectionName)
where TOptions : class
where TOptionsValidator : class, IValidateOptions<TOptions>, new()
{
var configurationSection = configuration.GetSection(sectionName);
string optionsPath = configurationSection.Path;
services
.AddOptions<TOptions>()
.BindConfiguration(optionsPath, options =>
{
options.ErrorOnUnknownConfiguration = true; // Strict binding
})
.ValidateDataAnnotations() // Validate DataAnnotations
.ValidateOnStart(); // Fail-fast validation
// Register custom validator
services.AddSingleton<IValidateOptions<TOptions>, TOptionsValidator>();
services.ActivateSingleton<IValidateOptions<TOptions>>();
// Validate immediately and throw if invalid
var options = configurationSection.Get<TOptions>()
?? throw new ArgumentException($"Failed to bind section '{sectionName}'.");
var validator = new TOptionsValidator();
var result = validator.Validate(string.Empty, options);
if (result.Failed)
{
throw new OptionsValidationException(
sectionName,
typeof(TOptions),
result.Failures);
}
return options;
}
Key Features:
- ErrorOnUnknownConfiguration: Fails if config contains unknown properties
- ValidateDataAnnotations(): Validates [Required], [Range], etc.
- ValidateOnStart(): Ensures validation occurs during startup
- Immediate validation: Throws exception if configuration is invalid
Using Options in Services¶
Options are injected via dependency injection:
public class MyService
{
private readonly MicroserviceOptions options;
public MyService(IOptions<MicroserviceOptions> options)
{
this.options = options.Value
?? throw new ArgumentNullException(nameof(options));
}
public string GetServiceName() => this.options.MicroserviceName;
}
Options Interface Types:
| Interface | Lifetime | Use Case |
|---|---|---|
IOptions<T> |
Singleton | Immutable config, loaded once at startup |
IOptionsSnapshot<T> |
Scoped | Per-request config (supports reload) |
IOptionsMonitor<T> |
Singleton | Monitors config changes, supports reload |
Example with IOptionsMonitor (for dynamic config):
public class DynamicService
{
private readonly IOptionsMonitor<MicroserviceOptions> optionsMonitor;
public DynamicService(IOptionsMonitor<MicroserviceOptions> optionsMonitor)
{
this.optionsMonitor = optionsMonitor;
// Subscribe to changes
this.optionsMonitor.OnChange(options =>
{
this.logger.LogInformation("Configuration changed: {Name}", options.MicroserviceName);
});
}
public string GetCurrentName() => this.optionsMonitor.CurrentValue.MicroserviceName;
}
Complex Configuration Structures¶
Nested Objects¶
Configuration can contain nested objects:
{
"Orleans": {
"OrleansEndpoints": {
"SiloPort": 11111,
"GatewayPort": 30000
},
"Connection": {
"ConnectionString": "..."
}
}
}
Options class:
public sealed class OrleansOptions
{
public const string OrleansOptionsSectionName = "Orleans";
[Required]
[ValidateObjectMembers] // Validates nested object
required public OrleansEndpointsOptions OrleansEndpoints { get; set; }
[Required]
[ValidateObjectMembers]
required public OrleansConnectionOptions Connection { get; set; }
}
public sealed class OrleansEndpointsOptions
{
[Required]
[Range(1024, 65535)]
required public int SiloPort { get; set; }
[Required]
[Range(1024, 65535)]
required public int GatewayPort { get; set; }
}
Arrays and Lists¶
Arrays are bound automatically:
public sealed class LocalizationOptions
{
[Required]
[MinLength(1)]
required public List<string> SupportedCultures { get; set; }
}
Validation¶
DataAnnotations Validation¶
Standard DataAnnotations attributes are supported:
| Attribute | Purpose | Example |
|---|---|---|
[Required] |
Ensures property is not null/empty | [Required] public string Name { get; set; } |
[Range] |
Validates numeric range | [Range(0, 100)] public int Percentage { get; set; } |
[StringLength] |
Validates string length | [StringLength(100)] public string Description { get; set; } |
[Url] |
Validates URL format | [Url] public string Endpoint { get; set; } |
[EmailAddress] |
Validates email format | [EmailAddress] public string Email { get; set; } |
[ValidateObjectMembers] |
Validates nested objects | [ValidateObjectMembers] public NestedOptions Nested { get; set; } |
Custom Validation¶
For complex validation logic, implement IValidateOptions<T>:
public class CustomValidator : IValidateOptions<MyOptions>
{
public ValidateOptionsResult Validate(string name, MyOptions options)
{
var failures = new List<string>();
if (options.StartDate >= options.EndDate)
{
failures.Add("StartDate must be before EndDate.");
}
if (failures.Count > 0)
{
return ValidateOptionsResult.Fail(failures);
}
return ValidateOptionsResult.Success;
}
}
Register custom validator:
Fail-Fast Validation¶
All options are validated at startup via .ValidateOnStart():
Benefits: - Prevents misconfigured services from starting - Clear error messages at startup - No runtime surprises - Easier debugging
Example Failure:
If MicroserviceName is missing:
System.InvalidOperationException: Failed to validate options: MicroserviceOptions
The MicroserviceName field is required.
The application will not start, preventing undefined behavior.
Azure App Configuration Integration¶
Azure App Configuration provides centralized, dynamic configuration management. When enabled, it serves as an additional configuration provider that overrides file-based configuration.
Overview¶
Azure App Configuration offers: - Centralized Management: Single source of truth for configuration across services - Dynamic Updates: Change configuration without redeployment - Environment Labels: Multi-environment configuration support - Feature Flags: Built-in feature flag management - Key Vault Integration: Secure secret management
See Azure App Configuration for comprehensive documentation.
Quick Setup¶
Azure App Configuration is conditionally integrated:
#if UseAzureAppConfigurationAsAdditionalConfigurationProvider
configurationBuilder.AddAzureAppConfiguration(options =>
{
string? connectionString = configurationRoot.GetConnectionString("AzureAppConfiguration");
options.Connect(connectionString)
// Select keys matching pattern
.Select("ConnectSoft.MicroserviceTemplate:*", LabelFilter.Null)
// Configure refresh
.ConfigureRefresh(refresh =>
{
refresh.Register("ConnectSoft.MicroserviceTemplate:Settings:Sentinel", refreshAll: true);
refresh.SetRefreshInterval(TimeSpan.FromMinutes(30));
})
// Configure retry
.ConfigureClientOptions(options =>
{
options.Retry.MaxRetries = 1;
});
// Feature flags (optional)
#if UseAzureAppConfigurationAsFeatureFlagsProvider
options.UseFeatureFlags(featureFlagsOptions =>
{
featureFlagsOptions.Select("ConnectSoft.MicroserviceTemplate:*", LabelFilter.Null);
featureFlagsOptions.SetRefreshInterval(TimeSpan.FromMinutes(30));
});
#endif
});
#endif
Configuration Priority¶
Azure App Configuration sits in the configuration hierarchy as follows:
Command-Line Arguments (Highest Priority)
↓
Environment Variables
↓
Azure App Configuration (if enabled)
↓
appsettings.{Environment}.json
↓
appsettings.json (Base)
Dynamic Configuration with IOptionsMonitor¶
Use IOptionsMonitor<T> to access dynamically refreshing configuration:
public class ConfigurableService
{
private readonly IOptionsMonitor<DynamicOptions> optionsMonitor;
public ConfigurableService(IOptionsMonitor<DynamicOptions> optionsMonitor)
{
this.optionsMonitor = optionsMonitor;
// Subscribe to changes
this.optionsMonitor.OnChange(options =>
{
this.logger.LogInformation("Configuration updated at {Time}", DateTime.UtcNow);
});
}
public void Process()
{
// Always gets latest value (even after refresh)
var currentConfig = this.optionsMonitor.CurrentValue;
// ... use config
}
}
Middleware for Dynamic Refresh¶
Enable dynamic refresh middleware in the pipeline:
#if UseAzureAppConfigurationAsAdditionalConfigurationProvider
application.UseMicroserviceAzureAppConfiguration();
#endif
This middleware:
- Polls for sentinel key changes at configured interval
- Refreshes configuration when sentinel key changes
- Updates IOptionsMonitor<T> instances automatically
- Triggers change notifications for subscribers
For detailed information on Azure App Configuration, including: - Connection string configuration - Key selection and naming conventions - Environment labels and multi-environment support - Sentinel key strategy - Feature flags integration - Security considerations - Troubleshooting
See the Azure App Configuration documentation.
Testing¶
Unit Testing with In-Memory Configuration¶
Override configuration in tests:
[TestMethod]
public void Service_WithCustomConfig_ShouldWork()
{
// Arrange
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["Microservice:MicroserviceName"] = "TestService",
["Microservice:StartupWarmupSeconds"] = "0"
})
.Build();
var services = new ServiceCollection();
services.AddMicroserviceOptions<MicroserviceOptions>(config, "Microservice");
services.AddScoped<MyService>();
var provider = services.BuildServiceProvider();
// Act
var service = provider.GetRequiredService<MyService>();
// Assert
Assert.AreEqual("TestService", service.GetServiceName());
}
Testing Validation Failures¶
Verify validation behavior:
[TestMethod]
public void InvalidOptions_ShouldThrowAtStartup()
{
// Arrange
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
// Missing required MicroserviceName
["Microservice:StartupWarmupSeconds"] = "20"
})
.Build();
var services = new ServiceCollection();
services.AddMicroserviceOptions<MicroserviceOptions>(config, "Microservice");
// Act & Assert
Assert.ThrowsException<OptionsValidationException>(() =>
{
services.BuildServiceProvider().GetRequiredService<IOptions<MicroserviceOptions>>();
});
}
Integration Testing¶
Use WebApplicationFactory with configuration overrides:
public class MyServiceTests
{
[TestMethod]
public async Task GetServiceName_ShouldReturnConfiguredName()
{
// Arrange
var factory = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
builder.ConfigureAppConfiguration((context, config) =>
{
config.AddInMemoryCollection(new Dictionary<string, string>
{
["Microservice:MicroserviceName"] = "TestService"
});
});
});
var client = factory.CreateClient();
// Act
var response = await client.GetAsync("/api/service/name");
// Assert
response.EnsureSuccessStatusCode();
}
}
Best Practices¶
Do's¶
-
Use Options Pattern for All Configuration
-
Validate at Startup
-
Use Descriptive Section Names
-
Provide Sensible Defaults
-
Document Configuration Properties
-
Separate Secrets from Configuration
Don'ts¶
-
Don't Use Magic Strings
-
Don't Skip Validation
-
Don't Store Secrets in Files
-
Don't Use IConfiguration Directly in Business Logic
-
Don't Forget Environment-Specific Overrides
Common Configuration Patterns¶
Connection Strings¶
Connection strings are managed separately:
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=MyDb;...",
"AzureAppConfiguration": "Endpoint=https://..."
}
}
Access via:
Feature Flags¶
Feature flags configuration (see Feature Flags):
Logging Configuration¶
Serilog configuration:
{
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"System": "Warning"
}
},
"WriteTo": [
{ "Name": "Console" },
{
"Name": "Seq",
"Args": {
"serverUrl": "http://localhost:5341"
}
}
]
}
}
Troubleshooting¶
Issue: Configuration Not Loading¶
Symptoms: Options are null or default values.
Solutions:
1. Verify section name matches exactly (case-sensitive)
2. Check configuration file is in correct location
3. Verify appsettings.json is marked as "Copy to Output Directory"
4. Check environment variable is set correctly
Issue: Validation Failures at Startup¶
Symptoms: OptionsValidationException during startup.
Solutions:
1. Check error message for missing required properties
2. Verify [Required] attributes match actual requirements
3. Ensure environment-specific files override base correctly
4. Check DataAnnotations (Range, StringLength, etc.)
Issue: Azure App Configuration Not Refreshing¶
Symptoms: Configuration changes in Azure don't reflect in app.
Solutions:
1. Verify UseMicroserviceAzureAppConfiguration() middleware is registered
2. Check sentinel key is being updated
3. Verify refresh interval is appropriate
4. Check connection string is valid
5. Review logs for refresh errors
Issue: Environment-Specific Override Not Applied¶
Symptoms: Base config values used instead of environment-specific.
Solutions:
1. Verify ASPNETCORE_ENVIRONMENT or DOTNET_ENVIRONMENT is set
2. Check environment file name matches: appsettings.{Environment}.json
3. Verify file is optional but loaded (check logs)
4. Check environment variable casing (case-insensitive)
Summary¶
Configuration in the ConnectSoft Microservice Template provides:
- ✅ Strong Typing: Options Pattern with POCO classes
- ✅ Validation: Startup-time validation with DataAnnotations and custom validators
- ✅ Environment Awareness: Layered configuration with environment-specific overrides
- ✅ Fail-Fast: Services won't start with invalid configuration
- ✅ Centralized Management: Azure App Configuration integration for cloud deployments
- ✅ Dynamic Refresh: Runtime configuration updates without redeployment
- ✅ Type Safety: IntelliSense support and compile-time checking
- ✅ Testability: Easy to override in unit and integration tests
By following these patterns, teams can:
- Deploy Safely: Validate configuration before service starts
- Maintain Consistently: Strong typing prevents configuration drift
- Adapt Easily: Environment-specific overrides for different deployment contexts
- Scale Confidently: Centralized configuration for multi-service deployments
- Debug Quickly: Clear validation errors at startup
Configuration is the foundation that enables microservices to operate correctly across diverse environments while maintaining safety, consistency, and observability.