Lightweight .NET configuration validation library with fail-fast startup validation
Fox.ConfigKit is a lightweight library for validating IOptions<T> configurations at application startup. Catch configuration errors before they cause runtime failures with fluent, expressive validation rules.
- Why Fox.ConfigKit?
- Features
- Installation
- Quick Start
- Validation Rules
- Advanced Scenarios
- Sample Application
- Design Principles
- Requirements
- Contributing
- License
Traditional approach:
// ❌ Configuration errors discovered at runtime
public class MyService
{
private readonly MyConfig config;
public void DoWork()
{
// Crashes here if config.ApiUrl is null or invalid!
var client = new HttpClient { BaseAddress = new Uri(config.ApiUrl) };
}
}Fox.ConfigKit approach:
// ✅ Configuration errors caught at startup
builder.Services.AddConfigKit<MyConfig>("MyConfig")
.NotEmpty(c => c.ApiUrl, "API URL is required")
.UrlReachable(c => c.ApiUrl, message: "API is not reachable")
.ValidateOnStartup();
// Application won't start if configuration is invalid!- ✅ Fail-Fast Validation - Catch configuration errors at application startup
- ✅ Fluent API - Intuitive, type-safe configuration validation
- ✅ Generic Type Support - Validate any
IComparable<T>type (int, decimal, DateTime, TimeSpan, etc.) - ✅ Conditional Validation - Apply rules based on environment or other properties
- ✅ File System Validation - Verify directories and files exist
- ✅ Network Validation - Check URL reachability at startup
- ✅ Security Validation - Detect plain-text secrets in configuration
- ✅ IOptions Integration - Seamless integration with
Microsoft.Extensions.Options - ✅ No Third-Party Dependencies - Only uses core Microsoft.Extensions packages
- ✅ Multi-Targeting - Supports .NET 8.0, .NET 9.0, and .NET 10.0
dotnet add package Fox.ConfigKitNuGet Package Manager:
Install-Package Fox.ConfigKit
PackageReference:
<PackageReference Include="Fox.ConfigKit" Version="1.0.4" />public sealed class DatabaseConfig
{
public string ConnectionString { get; set; } = string.Empty;
public int MaxPoolSize { get; set; }
public int CommandTimeoutSeconds { get; set; }
}{
"Database": {
"ConnectionString": "Server=localhost;Database=MyDb;User Id=sa;Password=YourPassword123;",
"MaxPoolSize": 100,
"CommandTimeoutSeconds": 30
},
"Api": {
"BaseUrl": "https://api.example.com",
"ApiKey": "your-api-key-here",
"TimeoutSeconds": 30,
"MaxRetries": 3
},
"Logging": {
"LogDirectory": "C:\\Logs\\MyApp",
"MinimumLevel": "Information",
"RetentionDays": 30
}
}using Fox.ConfigKit;
using Fox.ConfigKit.Validation;
var builder = WebApplication.CreateBuilder(args);
// Register configuration with validation
builder.Services.AddConfigKit<DatabaseConfig>("Database")
.NotEmpty(c => c.ConnectionString, "Connection string is required")
.InRange(c => c.MaxPoolSize, 1, 1000, "Max pool size must be between 1 and 1000")
.InRange(c => c.CommandTimeoutSeconds, 1, 600, "Command timeout must be between 1 and 600 seconds")
.ValidateOnStartup();
var app = builder.Build();
app.Run();public class MyService
{
private readonly DatabaseConfig config;
public MyService(IOptions<DatabaseConfig> config)
{
this.config = config.Value; // Already validated at startup!
}
}| Method | Description |
|---|---|
NotEmpty(selector, message) |
Ensures string is not null or empty |
NotNull(selector, message) |
Ensures value is not null |
MatchesPattern(selector, regex, message) |
Validates string against regex pattern |
Example:
builder.Services.AddConfigKit<AppConfig>("App")
.NotEmpty(c => c.Name, "Application name is required")
.MatchesPattern(c => c.Version, @"^\d+\.\d+\.\d+$", "Version must be X.Y.Z format")
.ValidateOnStartup();All comparison validation methods support any IComparable<T> type including int, decimal, DateTime, TimeSpan, and more.
| Method | Description | Boundary |
|---|---|---|
GreaterThan(selector, min, message) |
Ensures value is greater than minimum | Exclusive (>) |
LessThan(selector, max, message) |
Ensures value is less than maximum | Exclusive (<) |
Minimum(selector, min, message) |
Ensures value is at least minimum | Inclusive (>=) |
Maximum(selector, max, message) |
Ensures value is at most maximum | Inclusive (<=) |
InRange(selector, min, max, message) |
Ensures value is within range | Inclusive (>=, <=) |
Example with integers:
builder.Services.AddConfigKit<ApiConfig>("Api")
.GreaterThan(c => c.Port, 1024, "Port must be > 1024")
.Maximum(c => c.Port, 65535, "Port must be <= 65535")
.InRange(c => c.MaxRetries, 0, 10, "Max retries must be between 0 and 10")
.ValidateOnStartup();Example with decimals:
builder.Services.AddConfigKit<ProductConfig>("Product")
.Minimum(c => c.Price, 0.01m, "Price must be at least $0.01")
.Maximum(c => c.Discount, 0.5m, "Discount cannot exceed 50%")
.ValidateOnStartup();Example with DateTime:
builder.Services.AddConfigKit<CampaignConfig>("Campaign")
.Minimum(c => c.StartDate, DateTime.Today, "Campaign must start today or later")
.LessThan(c => c.EndDate, new DateTime(2025, 12, 31), "Campaign must end before 2025")
.ValidateOnStartup();Example with TimeSpan:
builder.Services.AddConfigKit<ApiConfig>("Api")
.GreaterThan(c => c.Timeout, TimeSpan.Zero, "Timeout must be positive")
.Maximum(c => c.Timeout, TimeSpan.FromMinutes(5), "Timeout cannot exceed 5 minutes")
.ValidateOnStartup();| Method | Description |
|---|---|
FileExists(selector, message) |
Validates file exists at path |
DirectoryExists(selector, message) |
Validates directory exists at path |
Example:
builder.Services.AddConfigKit<LoggingConfig>("Logging")
.NotEmpty(c => c.LogDirectory, "Log directory is required")
.DirectoryExists(c => c.LogDirectory, message: "Log directory does not exist")
.ValidateOnStartup();| Method | Description |
|---|---|
UrlReachable(selector, timeout, message) |
Validates URL is reachable via HTTP HEAD request |
Example:
builder.Services.AddConfigKit<ExternalApiConfig>("ExternalApi")
.NotEmpty(c => c.BaseUrl, "API URL is required")
.UrlReachable(c => c.BaseUrl, timeout: TimeSpan.FromSeconds(10), message: "API is not reachable")
.ValidateOnStartup();| Method | Description |
|---|---|
NoPlainTextSecrets(selector, message) |
Detects plain-text secrets using pattern matching |
Example:
builder.Services.AddConfigKit<ApiConfig>("Api")
.NotEmpty(c => c.ApiKey, "API key is required")
.NoPlainTextSecrets(c => c.ApiKey, "API key appears to be a plain-text secret")
.ValidateOnStartup();Detected secret patterns:
- OpenAI API keys (
sk-...) - Google API keys (
AIza...) - AWS Access Keys (
AKIA...) - Bearer tokens
- Generic Base64 secrets
- Hexadecimal secrets (64 chars)
| Method | Description |
|---|---|
EnvironmentVariableExists(selector, message) |
Validates environment variable exists |
Example:
builder.Services.AddConfigKit<AppConfig>("App")
.NotEmpty(c => c.EnvironmentName, "Environment name is required")
.EnvironmentVariableExists(c => c.RequiredEnvVar, "Required environment variable not set")
.ValidateOnStartup();Apply validation rules conditionally based on configuration values:
builder.Services.AddConfigKit<SecurityConfig>("Security")
.NotEmpty(c => c.Environment, "Environment is required")
.When(c => c.Environment == "Production", b =>
{
// These rules only apply in production
b.NotEmpty(c => c.CertificatePath, "Certificate is required in production")
.FileExists(c => c.CertificatePath, message: "Certificate file not found");
})
.When(c => c.RequireHttps, b =>
{
// These rules only apply when HTTPS is required
b.FileExists(c => c.CertificatePath, message: "HTTPS requires certificate");
})
.ValidateOnStartup();public sealed class DatabaseConfig
{
public string ConnectionString { get; set; } = string.Empty;
public bool RequireSsl { get; set; }
}
builder.Services.AddConfigKit<DatabaseConfig>("Database")
.NotEmpty(c => c.ConnectionString, "Connection string is required")
.When(c => c.RequireSsl, b =>
{
b.MatchesPattern(c => c.ConnectionString, "Encrypt=True|Encrypt=true",
"SSL required but connection string does not specify Encrypt=True");
})
.ValidateOnStartup();Create custom validation rules by inheriting from ValidationRuleBase:
using Fox.ConfigKit.Validation;
internal sealed class CustomRule<T> : ValidationRuleBase, IValidationRule<T> where T : class
{
private readonly Func<T, string?> getValue;
private readonly string propertyName;
private readonly string? message;
public CustomRule(Expression<Func<T, string?>> selector, string? message = null)
{
this.getValue = selector.Compile();
this.propertyName = GetPropertyName(selector);
this.message = message;
}
public ConfigValidationError? Validate(T options, string sectionName)
{
var value = getValue(options);
if (string.IsNullOrEmpty(value))
{
var key = $"{sectionName}:{propertyName}";
var errorMessage = message ?? $"{propertyName} is invalid";
return new ConfigValidationError(key, errorMessage, value, ["Provide a valid value"]);
}
return null;
}
}
// Extension method
public static class CustomValidationExtensions
{
public static ConfigValidationBuilder<T> MyCustomValidation<T>(
this ConfigValidationBuilder<T> builder,
Expression<Func<T, string?>> selector,
string? message = null) where T : class
{
return builder.AddRule(new CustomRule<T>(selector, message));
}
}For applications using Fox.ResultKit, the Fox.ConfigKit.ResultKit integration package enables functional-style configuration validation with Railway Oriented Programming.
dotnet add package Fox.ConfigKit.ResultKitInstead of exception-based validation at startup, use Result<T> for explicit error handling:
using Fox.ConfigKit.ResultKit;
using Fox.ResultKit;
var builder = WebApplication.CreateBuilder(args);
// Register configuration WITHOUT ValidateOnStartup()
builder.Services.AddConfigKit<ApplicationConfig>("Application")
.NotEmpty(c => c.Name, "Application name is required")
.Minimum(c => c.MaxConcurrentRequests, 1)
.Maximum(c => c.MaxConcurrentRequests, 1000);
// ← No .ValidateOnStartup() call!
builder.Services.AddConfigKit<DatabaseConfig>("Database")
.NotEmpty(c => c.ConnectionString);
var app = builder.Build();
// Explicit validation with Result pattern
var appConfig = app.Services.GetRequiredService<IOptions<ApplicationConfig>>().Value;
var dbConfig = app.Services.GetRequiredService<IOptions<DatabaseConfig>>().Value;
// Composable validation pipeline
var startupResult = ConfigValidator.Validate<ApplicationConfig>()
.NotEmpty(c => c.Name, "Application name is required")
.Minimum(c => c.MaxConcurrentRequests, 1)
.ToResult(appConfig)
.Bind(_ => ConfigValidator.Validate<DatabaseConfig>()
.NotEmpty(c => c.ConnectionString, "Connection string required")
.ToResult(dbConfig));
// Functional error handling
startupResult.Match(
onSuccess: _ =>
{
app.Logger.LogInformation("All configurations valid");
app.Run();
},
onFailure: error =>
{
app.Logger.LogCritical("Configuration validation failed: {Error}", error.Message);
Environment.Exit(1);
}
);Use ToErrorsResult() to collect all validation errors instead of stopping at the first one:
var validationResult = ConfigValidator.Validate<ApplicationConfig>()
.NotEmpty(c => c.Name, "Name is required")
.InRange(c => c.MaxConcurrentRequests, 1, 1000)
.NotEmpty(c => c.Version, "Version is required")
.ToErrorsResult(appConfig);
if (validationResult.IsFailure)
{
app.Logger.LogCritical("Configuration has {Count} errors:", validationResult.Errors.Count);
foreach (var error in validationResult.Errors)
{
app.Logger.LogError(" - {ErrorMessage}", error.Message);
}
Environment.Exit(1);
}
app.Run();| Scenario | Use ValidateOnStartup() |
Use ToResult() / Result Pattern |
|---|---|---|
| Simple ASP.NET Core app | ✅ Recommended (fail-fast) | ❌ Overkill |
| Console/Worker apps | ❌ No DI startup | ✅ Recommended |
| Complex startup logic | 🟡 Limited control | ✅ Full control over errors |
| Multiple configs to validate | 🟡 Sequential exceptions | ✅ Composable chain with Bind() |
| Need all errors at once | ❌ Stops at first error | ✅ ToErrorsResult() collects all |
| Unit testing validation | 🟡 Exception-based asserts | ✅ Result-based asserts |
| Graceful shutdown on error | ❌ Exception crash | ✅ Environment.Exit(1) |
| Already using Railway pattern | 🟡 Inconsistent style | ✅ Consistent functional style |
For console apps, scripts, or unit tests where DI is not available:
using Fox.ConfigKit.ResultKit;
// Load configuration from file/environment
var config = LoadConfigurationFromFile("appsettings.json");
// Validate without dependency injection
var result = ConfigValidator.Validate<AppConfig>()
.NotEmpty(c => c.Name, "Name is required")
.InRange(c => c.Port, 1, 65535, "Invalid port")
.ToResult(config);
return result.Match(
onSuccess: validConfig => RunApplication(validConfig),
onFailure: error =>
{
Console.WriteLine($"Configuration error: {error}");
return 1; // Exit code
}
);Learn more: See the Fox.ConfigKit.ResultKit README for detailed examples and advanced patterns.
A comprehensive sample WebApi application is available in the repository demonstrating:
- ✅ Application-wide configuration validation
- ✅ Database connection string validation
- ✅ External API configuration with URL reachability checks
- ✅ File system validation for log directories
- ✅ Environment-based conditional validation
- ✅ Security configuration with certificate validation
Run the sample:
cd samples/Fox.ConfigKit.Samples.WebApi
dotnet runExplore:
- Open
https://localhost:5001/swagger - View validated configurations via
/api/configuration/*endpoints - See sample README for details
- Fail-Fast - Catch configuration errors at startup, not at runtime
- Explicit Validation - Make validation rules clear and discoverable
- Type-Safe - Leverage C# type system for compile-time safety
- Fluent API - Intuitive, chainable validation rules
- No Third-Party Dependencies - Only core Microsoft.Extensions packages, no external libraries
- Developer-Friendly - Clear error messages, excellent IntelliSense
- .NET 8.0 or higher
- C# 12 or higher (for modern language features)
- Nullable reference types enabled (recommended)
Fox.ConfigKit is intentionally lightweight and feature-focused. The goal is to remain a simple library with minimal dependencies for configuration validation.
- ✅ Bug fixes - Issues with existing functionality
- ✅ Documentation improvements - Clarifications, examples, typo fixes
- ✅ Performance optimizations - Without breaking API compatibility
- ✅ New validation rules - Following existing patterns
- ❌ New dependencies or third-party packages
- ❌ Large feature additions that increase complexity
- ❌ Breaking API changes
If you want to propose a significant change, please open an issue first to discuss whether it aligns with the project's philosophy.
The project enforces a strict build policy to ensure code quality:
- ❌ No errors allowed - Build must be error-free
- ❌ No warnings allowed - All compiler warnings must be resolved
- ❌ No messages allowed - Informational messages must be suppressed or addressed
All pull requests must pass this requirement.
- Follow the existing code style (see
.github/copilot-instructions.md) - Use file-scoped namespaces
- Enable nullable reference types
- Add XML documentation for public APIs
- Write unit tests for new features
- Use expression-bodied members for simple properties
- Auto-properties preferred over backing fields
- Fork the repository
- Create a feature branch from
main - Follow the coding standards in
.github/copilot-instructions.md - Ensure all tests pass (
dotnet test) - Submit a pull request
This project is licensed under the MIT License - see the LICENSE.txt file for details.
Károly Akácz
- GitHub: @akikari
- Repository: Fox.ConfigKit
Current version: 1.0.4
See CHANGELOG.md for version history.
- Fox.ResultKit - Lightweight Result pattern library for Railway Oriented Programming
- Fox.ConfigKit.ResultKit - Integration package for using ConfigKit with Result pattern
For issues, questions, or feature requests, please open an issue in the GitHub repository.