Real-world demonstration of configuration validation with fail-fast startup in ASP.NET Core
This sample application demonstrates how to use Fox.ConfigKit to validate application configurations at startup, catching errors before they cause runtime issues.
- Overview
- Quick Start
- Project Structure
- Configuration Examples
- API Endpoints
- Fail-Fast Behavior
- Testing Scenarios
- Real-World Benefits
This sample shows how to:
- ✅ Configure and validate application settings at startup
- ✅ Use fail-fast validation to catch configuration errors early
- ✅ Apply conditional validation rules based on environment
- ✅ Validate file system paths, URLs, and security settings
- ✅ Use validated configurations in controllers
- ✅ Detect plain-text secrets in configuration values
- .NET 8.0 SDK or later
- Valid log directory (create
C:\Logs\ConfigKitSampleon Windows or update path in appsettings)
ExternalApi.BaseUrl is set to https://api-dev.example.com (a non-existent example URL). The application will fail at startup because the URL is not reachable. This demonstrates the UrlReachable() validation. To make the sample run successfully:
- Comment out the
.UrlReachable()line inProgram.cs(line 33), OR - Change the URL to a real, reachable API (e.g.,
https://jsonplaceholder.typicode.com)
-
Create log directory (Windows)
New-Item -Path "C:\Logs\ConfigKitSample\Dev" -ItemType Directory -Force
Or (Linux/macOS)
mkdir -p ~/logs/ConfigKitSample/Dev -
Fix External API URL (choose one option)
- Option A: Comment out
.UrlReachable()inProgram.cs - Option B: Change
BaseUrlinappsettings.Development.jsonto a real URL
- Option A: Comment out
-
Run the application
cd samples/Fox.ConfigKit.Samples.WebApi dotnet run -
Access Swagger UI
https://localhost:5001/swagger -
Explore the
/api/configuration/*endpoints
Configuration/
├── ApplicationConfig.cs # Application-wide settings (Minimum/Maximum validation)
├── DatabaseConfig.cs # Database connection settings (InRange validation)
├── ExternalApiConfig.cs # External API integration (GreaterThan/LessThan validation)
├── LoggingConfig.cs # Custom logging configuration (File system validation)
├── SecurityConfig.cs # Security and SSL/TLS settings (Conditional validation)
└── CampaignConfig.cs # Marketing campaign with generic types (decimal, DateTime, TimeSpan)
Controllers/
└── ConfigurationController.cs # Endpoints to view validated configurations
appsettings.json # Base configuration
appsettings.Development.json # Development-specific overrides
appsettings.Production.json # Production configuration example
Program.cs # Startup with Fox.ConfigKit validation (6 examples)
Demonstrates: Minimum(), Maximum() with inclusive boundaries (>=, <=)
builder.Services.AddConfigKit<ApplicationConfig>("Application")
.NotEmpty(c => c.Name, "Application name is required")
.MatchesPattern(c => c.Version, @"^\d+\.\d+\.\d+$", "Version must be in format X.Y.Z")
.Minimum(c => c.MaxConcurrentRequests, 1, "Max concurrent requests must be at least 1")
.Maximum(c => c.MaxConcurrentRequests, 1000, "Max concurrent requests cannot exceed 1000")
.ValidateOnStartup();Configuration class:
public sealed class ApplicationConfig
{
public string Name { get; set; } = string.Empty;
public string Version { get; set; } = string.Empty;
public int MaxConcurrentRequests { get; set; }
public int RequestTimeoutSeconds { get; set; }
public bool EnableMetrics { get; set; }
}appsettings.json:
{
"Application": {
"Name": "Fox.ConfigKit Sample API",
"Version": "1.0.0",
"MaxConcurrentRequests": 100,
"RequestTimeoutSeconds": 30,
"EnableMetrics": true
}
}Demonstrates: Traditional InRange() validation with inclusive boundaries
builder.Services.AddConfigKit<DatabaseConfig>("Database")
.NotEmpty(c => c.ConnectionString, "Database connection string is required")
.InRange(c => c.CommandTimeoutSeconds, 1, 600, "Command timeout must be between 1 and 600 seconds")
.InRange(c => c.MaxPoolSize, 1, 1000, "Max pool size must be between 1 and 1000")
.ValidateOnStartup();Configuration class:
public sealed class DatabaseConfig
{
public string ConnectionString { get; set; } = string.Empty;
public int CommandTimeoutSeconds { get; set; }
public int MaxPoolSize { get; set; }
public bool EnableSensitiveDataLogging { get; set; }
public bool RequireSsl { get; set; }
}appsettings.json:
{
"Database": {
"ConnectionString": "Server=localhost;Database=SampleDb;User Id=sa;Password=YourStrongPassword123!;TrustServerCertificate=True",
"CommandTimeoutSeconds": 30,
"MaxPoolSize": 100,
"EnableSensitiveDataLogging": false,
"RequireSsl": false
}
}Demonstrates: GreaterThan(), LessThan() with exclusive boundaries (>, <)
builder.Services.AddConfigKit<ExternalApiConfig>("ExternalApi")
.NotEmpty(c => c.BaseUrl, "External API base URL is required")
.NotEmpty(c => c.ApiKey, "External API key is required")
.NoPlainTextSecrets(c => c.ApiKey, "API key appears to be a plain-text secret")
.GreaterThan(c => c.TimeoutSeconds, 0, "API timeout must be greater than 0")
.LessThan(c => c.TimeoutSeconds, 600, "API timeout must be less than 600 seconds")
.ValidateOnStartup();Configuration class:
public sealed class ExternalApiConfig
{
public string BaseUrl { get; set; } = string.Empty;
public string ApiKey { get; set; } = string.Empty;
public int TimeoutSeconds { get; set; }
public int MaxRetries { get; set; }
}appsettings.json:
{
"ExternalApi": {
"BaseUrl": "https://api.example.com",
"ApiKey": "your-api-key-here",
"TimeoutSeconds": 30,
"MaxRetries": 3
}
}Demonstrates: DirectoryExists() with fail-fast behavior
builder.Services.AddConfigKit<LoggingConfig>("CustomLogging")
.NotEmpty(c => c.LogDirectory, "Log directory path is required")
.DirectoryExists(c => c.LogDirectory, message: "Log directory does not exist")
.InRange(c => c.RetentionDays, 1, 365, "Retention days must be between 1 and 365")
.ValidateOnStartup();Configuration class:
public sealed class LoggingConfig
{
public string LogDirectory { get; set; } = string.Empty;
public string MinimumLevel { get; set; } = "Information";
public int RetentionDays { get; set; }
public int MaxFileSizeMB { get; set; }
}appsettings.json:
{
"CustomLogging": {
"LogDirectory": "C:\\Logs\\ConfigKitSample",
"MinimumLevel": "Information",
"RetentionDays": 30,
"MaxFileSizeMB": 100
}
}Demonstrates: When() for environment-specific rules
builder.Services.AddConfigKit<SecurityConfig>("Security")
.NotEmpty(c => c.Environment, "Environment is required")
.When(c => c.Environment == "Production", b =>
{
b.NotEmpty(c => c.CertificatePath, "Certificate path is required in production")
.FileExists(c => c.CertificatePath, message: "Certificate file does not exist");
})
.ValidateOnStartup();Configuration class:
public sealed class SecurityConfig
{
public string Environment { get; set; } = "Development";
public string CertificatePath { get; set; } = string.Empty;
public string CertificatePassword { get; set; } = string.Empty;
public bool RequireHttps { get; set; }
public string[] AllowedOrigins { get; set; } = [];
}appsettings.json:
{
"Security": {
"Environment": "Development",
"CertificatePath": "",
"CertificatePassword": "",
"RequireHttps": false,
"AllowedOrigins": [
"http://localhost:3000",
"http://localhost:4200"
]
}
}Demonstrates: Generic validation with decimal, DateTime, and TimeSpan, plus GreaterThanProperty() for property-to-property comparison
builder.Services.AddConfigKit<CampaignConfig>("Campaign")
.NotEmpty(c => c.Name, "Campaign name is required")
.Minimum(c => c.StartDate, DateTime.Today, "Campaign must start today or later")
.GreaterThan(c => c.EndDate, DateTime.Today, "Campaign end date must be in the future")
.GreaterThanProperty(c => c.EndDate, c => c.StartDate, "Campaign end date must be after start date")
.Minimum(c => c.MinimumPurchaseAmount, 0.01m, "Minimum purchase amount must be at least $0.01")
.Maximum(c => c.MaximumDiscountPercentage, 0.75m, "Discount percentage cannot exceed 75%")
.GreaterThan(c => c.EmailReminderInterval, TimeSpan.Zero, "Email reminder interval must be positive")
.Maximum(c => c.CacheDuration, TimeSpan.FromHours(24), "Cache duration cannot exceed 24 hours")
.ValidateOnStartup();Configuration class:
public sealed class CampaignConfig
{
public string Name { get; set; } = string.Empty;
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public decimal MinimumPurchaseAmount { get; set; }
public decimal MaximumDiscountPercentage { get; set; }
public TimeSpan EmailReminderInterval { get; set; }
public TimeSpan CacheDuration { get; set; }
}appsettings.json:
{
"Campaign": {
"Name": "Summer Sale 2024",
"StartDate": "2024-06-01T00:00:00",
"EndDate": "2024-08-31T23:59:59",
"MinimumPurchaseAmount": 25.00,
"MaximumDiscountPercentage": 0.30,
"EmailReminderInterval": "7.00:00:00",
"CacheDuration": "01:00:00"
}
}Demonstrates: MinimumProperty() for comparing two properties (RecordsPerRun must be >= BatchSize)
builder.Services.AddConfigKit<MigrationConfig>("Migration")
.Minimum(c => c.RecordsPerRun, 0, "RecordsPerRun cannot be negative (0 = no limit)")
.InRange(c => c.BatchSize, 1, 100000, "Batch size must be between 1 and 100000")
.MinimumProperty(c => c.RecordsPerRun, c => c.BatchSize, "RecordsPerRun must be >= BatchSize (or 0 for no limit)")
.InRange(c => c.MaxRetryAttempts, 1, 10, "Max retry attempts must be between 1 and 10")
.InRange(c => c.RetryDelaySeconds, 1, 300, "Retry delay must be between 1 and 300 seconds")
.GreaterThan(c => c.CommandTimeoutSeconds, 0, "Command timeout must be greater than 0")
.ValidateOnStartup();Configuration class:
public sealed class MigrationConfig
{
public int RecordsPerRun { get; set; }
public int BatchSize { get; set; }
public int MaxRetryAttempts { get; set; }
public int RetryDelaySeconds { get; set; }
public int CommandTimeoutSeconds { get; set; }
}appsettings.json:
{
"Migration": {
"RecordsPerRun": 10000,
"BatchSize": 1000,
"MaxRetryAttempts": 3,
"RetryDelaySeconds": 5,
"CommandTimeoutSeconds": 30
}
}Demonstrates: Validating each item in a collection, including LINQ filtering support
builder.Services.AddConfigKit<ServerConfig>("Servers")
.InRange(c => c.MaxRetries, 0, 10, "Max retries must be between 0 and 10")
.InRange(c => c.TimeoutSeconds, 1, 300, "Timeout must be between 1 and 300 seconds")
.ValidateEach(c => c.Endpoints,
itemBuilder => itemBuilder
.NotEmpty(e => e.Name, "Endpoint name is required")
.NotEmpty(e => e.Url, "Endpoint URL is required")
.InRange(e => e.Port, 1, 65535, "Port must be between 1 and 65535")
.InRange(e => e.HealthCheckIntervalSeconds, 5, 3600, "Health check interval must be between 5 and 3600 seconds"),
minCount: 1,
emptyMessage: "At least one endpoint must be configured")
.ValidateEach(c => c.Endpoints.Where(e => e.Enabled),
itemBuilder => itemBuilder
.GreaterThan(e => e.HealthCheckIntervalSeconds, 0, "Enabled endpoints must have positive health check interval"),
minCount: 1,
emptyMessage: "At least one enabled endpoint is required")
.ValidateOnStartup();Configuration class:
public sealed class ServerConfig
{
public List<ServerEndpoint> Endpoints { get; set; } = [];
public int MaxRetries { get; set; }
public int TimeoutSeconds { get; set; }
}
public sealed class ServerEndpoint
{
public string Name { get; set; } = string.Empty;
public string Url { get; set; } = string.Empty;
public int Port { get; set; }
public bool Enabled { get; set; }
public int HealthCheckIntervalSeconds { get; set; }
}appsettings.json:
{
"Servers": {
"MaxRetries": 3,
"TimeoutSeconds": 30,
"Endpoints": [
{
"Name": "Primary API",
"Url": "https://api.primary.example.com",
"Port": 443,
"Enabled": true,
"HealthCheckIntervalSeconds": 60
},
{
"Name": "Secondary API",
"Url": "https://api.secondary.example.com",
"Port": 443,
"Enabled": true,
"HealthCheckIntervalSeconds": 120
},
{
"Name": "Backup API",
"Url": "https://api.backup.example.com",
"Port": 8443,
"Enabled": false,
"HealthCheckIntervalSeconds": 0
}
]
}
}| Endpoint | Description |
|---|---|
GET /api/configuration/application |
View application configuration |
GET /api/configuration/database |
View database configuration (sensitive data hidden) |
GET /api/configuration/external-api |
View external API configuration |
GET /api/configuration/logging |
View logging configuration |
GET /api/configuration/security |
View security configuration |
GET /api/configuration/campaign |
View campaign configuration |
GET /api/configuration/migration |
View migration configuration with property comparison |
GET /api/configuration/servers |
View server endpoints configuration |
GET /api/configuration/all |
View summary of all configurations |
When you start the application, Fox.ConfigKit validates all configurations immediately. If any validation fails, the application will not start and you'll see detailed error messages:
Unhandled exception. Microsoft.Extensions.Options.OptionsValidationException:
- Database connection string is required
- Log directory does not exist
- External API base URL is not reachable
✅ Catch errors at startup, not at runtime
✅ No partial deployments with broken configurations
✅ Clear error messages for DevOps teams
✅ Prevent production incidents caused by misconfiguration
Test: Remove the Database.ConnectionString from appsettings.json
Expected result:
❌ Application fails to start with error: "Database connection string is required"
Test: Set Application.MaxConcurrentRequests to 2000 (exceeds limit of 1000)
Expected result:
❌ Application fails to start with error: "Max concurrent requests must be between 1 and 1000"
Test: Set CustomLogging.LogDirectory to a non-existent path
Expected result:
❌ Application fails to start with error: "Log directory does not exist"
Test: Set Security.Environment to "Production" but leave CertificatePath empty
Expected result:
❌ Application fails to start with error: "Certificate path is required in production"
Test: Set ExternalApi.BaseUrl to an unreachable URL
Expected result:
❌ Application fails to start with error: "External API base URL is not reachable"
Test: Set Migration.RecordsPerRun to 500 and Migration.BatchSize to 1000 (RecordsPerRun < BatchSize)
Expected result:
❌ Application fails to start with error: "RecordsPerRun must be >= BatchSize (or 0 for no limit)"
| Benefit | Description |
|---|---|
| Early Error Detection | Find configuration issues before deployment |
| Security | Detect plain-text secrets in configuration |
| Infrastructure Validation | Verify files, directories, and URLs exist |
| Environment-Specific Rules | Different validation for dev/staging/prod |
| Living Documentation | Configuration rules serve as living documentation |
| DevOps Friendly | Clear error messages for deployment automation |
| Feature | Example in Sample |
|---|---|
| String Validation | NotEmpty, MatchesPattern |
| Integer Validation | Minimum, Maximum, GreaterThan, LessThan, InRange |
| Decimal Validation | Minimum for prices, Maximum for percentages |
| DateTime Validation | Minimum for campaign dates, GreaterThan for future dates |
| TimeSpan Validation | GreaterThan for positive intervals, Maximum for durations |
| Property Comparison | GreaterThanProperty, LessThanProperty, MinimumProperty, MaximumProperty |
| Collection Validation | ValidateEach for endpoint lists with LINQ filtering |
| File System | DirectoryExists, FileExists |
| Network | UrlReachable |
| Security | NoPlainTextSecrets |
| Conditional Logic | When for environment-based rules |
Built with Fox.ConfigKit - Lightweight .NET configuration validation library