Skip to content

Commit e462a82

Browse files
author
Akácz Károly
committed
Release v1.2.0: Collection validation with ValidateEach
- Add ValidateEach() extension for validating collection items - Support LINQ filtering (Where, Select, etc.) - Add minCount validation with custom error messages - Add CollectionValidationRule with LINQ expression support - Add 30 comprehensive tests for collection validation - Add ServerConfig sample with endpoint validation - Update documentation with collection validation examples - Add ConfigValidator.Validate standalone example - Extract ServerEndpoint to separate file - Fix CA1860 analyzer warnings (use Count instead of Any) - Sync versions to 1.2.0 across projects
1 parent 8c97837 commit e462a82

14 files changed

Lines changed: 803 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
_No unreleased changes yet._
1111

12+
## [1.2.0] - 2026-04-03
13+
14+
### Added
15+
16+
#### Fox.ConfigKit
17+
- **Collection Validation**: New `ValidateEach<T, TItem>()` extension method for validating each item in a collection
18+
- Supports simple property access: `c => c.Endpoints`
19+
- Supports LINQ filtering: `c => c.Endpoints.Where(e => e.Enabled)`
20+
- Optional `minCount` parameter for ensuring minimum collection size
21+
- Optional `emptyMessage` for custom empty collection error messages
22+
- Nested validation rules: Apply full validation builder to each collection item
23+
- New `CollectionValidationRule<T, TItem>` internal rule with expression tree parsing
24+
- Handles `MemberExpression` for simple property access
25+
- Handles `MethodCallExpression` for LINQ methods (Where, Select, etc.)
26+
- Custom `ExtractCollectionName()` method for proper error messages
27+
- 30 comprehensive unit tests covering all collection validation scenarios
28+
29+
#### Fox.ConfigKit.ResultKit
30+
- Collection validation now available through `ConfigValidator.Validate<T>()` API
31+
- Full integration with `ToValidationResult()` for functional error handling
32+
33+
#### Documentation
34+
- Updated README.md with collection validation examples and LINQ filtering
35+
- Added comprehensive `ServerConfig` example in sample project with endpoint validation
36+
- New API endpoint: `GET /api/configuration/servers`
37+
- Updated samples README with Example 8: Collection Validation
38+
- Added collection validation to Key Validation Features table
39+
40+
### Changed
41+
- Version bumped from 1.1.0 to 1.2.0 for both Fox.ConfigKit and Fox.ConfigKit.ResultKit packages
42+
1243
## [1.1.0] - 2026-03-28
1344

1445
### Added

README.md

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ builder.Services.AddConfigKit<MyConfig>("MyConfig")
6363
-**Fail-Fast Validation** - Catch configuration errors at application startup
6464
-**Fluent API** - Intuitive, type-safe configuration validation
6565
-**Generic Type Support** - Validate any `IComparable<T>` type (int, decimal, DateTime, TimeSpan, etc.)
66+
-**Property Comparison** - Compare two properties within the same configuration object
67+
-**Collection Validation** - Validate each item in a collection with nested rules and LINQ support
6668
-**Conditional Validation** - Apply rules based on environment or other properties
6769
-**File System Validation** - Verify directories and files exist
6870
-**Network Validation** - Check URL reachability at startup
@@ -84,7 +86,7 @@ Install-Package Fox.ConfigKit
8486

8587
**PackageReference:**
8688
```xml
87-
<PackageReference Include="Fox.ConfigKit" Version="1.0.4" />
89+
<PackageReference Include="Fox.ConfigKit" Version="1.2.0" />
8890
```
8991

9092
## 🚀 Quick Start
@@ -259,6 +261,60 @@ builder.Services.AddConfigKit<ProductConfig>("Product")
259261
.ValidateOnStartup();
260262
```
261263

264+
### Collection Validation
265+
266+
Validate each item in a collection with nested validation rules. Supports simple property access and LINQ filtering.
267+
268+
| Method | Description |
269+
|--------|-------------|
270+
| `ValidateEach<T, TItem>(collectionSelector, configureItemValidator, minCount, emptyMessage)` | Validates each item in a collection with nested rules |
271+
272+
**Example with simple collection:**
273+
274+
```csharp
275+
builder.Services.AddConfigKit<ServerConfig>("Servers")
276+
.ValidateEach(c => c.Endpoints,
277+
itemBuilder => itemBuilder
278+
.NotEmpty(e => e.Name, "Endpoint name is required")
279+
.NotEmpty(e => e.Url, "Endpoint URL is required")
280+
.InRange(e => e.Port, 1, 65535, "Port must be between 1 and 65535"),
281+
minCount: 1,
282+
emptyMessage: "At least one endpoint must be configured")
283+
.ValidateOnStartup();
284+
```
285+
286+
**Example with LINQ filtering (only enabled items):**
287+
288+
```csharp
289+
builder.Services.AddConfigKit<ServerConfig>("Servers")
290+
.ValidateEach(c => c.Endpoints.Where(e => e.Enabled),
291+
itemBuilder => itemBuilder
292+
.GreaterThan(e => e.HealthCheckIntervalSeconds, 0, "Enabled endpoints must have positive health check interval"),
293+
minCount: 1,
294+
emptyMessage: "At least one enabled endpoint is required")
295+
.ValidateOnStartup();
296+
```
297+
298+
**Example with ConfigValidator (standalone, no DI):**
299+
300+
```csharp
301+
using Fox.ConfigKit.ResultKit;
302+
303+
var validationRules = ConfigValidator.Validate<FileLoggingConfig>()
304+
.NotEmpty(c => c.LogDirectory, "Log directory is required")
305+
.DirectoryExists(c => c.LogDirectory, "Log directory does not exist")
306+
.ValidateEach(c => c.LogDefinitions.Where(l => l.Enabled),
307+
itemBuilder => itemBuilder
308+
.NotEmpty(l => l.Name, "Log name is required")
309+
.NotEmpty(l => l.TableName, "Table name is required")
310+
.InRange(l => l.BatchSize, 1, 10000, "Batch size must be between 1 and 10000"),
311+
minCount: 1,
312+
emptyMessage: "At least one enabled log definition is required");
313+
314+
// Use with Result pattern (Fox.ConfigKit.ResultKit)
315+
var result = validationRules.ToResult(config);
316+
```
317+
262318
### File System Validation
263319

264320
| Method | Description |
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//==================================================================================================
2+
// Configuration for server endpoints with health check settings.
3+
// Demonstrates collection validation with ValidateEach for endpoint lists.
4+
//==================================================================================================
5+
6+
namespace Fox.ConfigKit.Samples.WebApi.Configuration;
7+
8+
public sealed class ServerConfig
9+
{
10+
public List<ServerEndpoint> Endpoints { get; set; } = [];
11+
public int MaxRetries { get; set; }
12+
public int TimeoutSeconds { get; set; }
13+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//==================================================================================================
2+
// Represents a single server endpoint with health check configuration.
3+
// Used as collection item in ServerConfig for endpoint validation demonstration.
4+
//==================================================================================================
5+
6+
namespace Fox.ConfigKit.Samples.WebApi.Configuration;
7+
8+
public sealed class ServerEndpoint
9+
{
10+
public string Name { get; set; } = string.Empty;
11+
public string Url { get; set; } = string.Empty;
12+
public int Port { get; set; }
13+
public bool Enabled { get; set; }
14+
public int HealthCheckIntervalSeconds { get; set; }
15+
}

samples/Fox.ConfigKit.Samples.WebApi/Controllers/ConfigurationController.cs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ public class ConfigurationController(
1717
IOptions<LoggingConfig> loggingConfig,
1818
IOptions<SecurityConfig> securityConfig,
1919
IOptions<CampaignConfig> campaignConfig,
20-
IOptions<MigrationConfig> migrationConfig) : ControllerBase
20+
IOptions<MigrationConfig> migrationConfig,
21+
IOptions<ServerConfig> serverConfig) : ControllerBase
2122
{
2223
private readonly ApplicationConfig applicationConfig = applicationConfig.Value;
2324
private readonly DatabaseConfig databaseConfig = databaseConfig.Value;
@@ -26,6 +27,7 @@ public class ConfigurationController(
2627
private readonly SecurityConfig securityConfig = securityConfig.Value;
2728
private readonly CampaignConfig campaignConfig = campaignConfig.Value;
2829
private readonly MigrationConfig migrationConfig = migrationConfig.Value;
30+
private readonly ServerConfig serverConfig = serverConfig.Value;
2931

3032
[HttpGet("application")]
3133
public IActionResult GetApplicationConfig()
@@ -118,6 +120,26 @@ public IActionResult GetMigrationConfig()
118120
});
119121
}
120122

123+
[HttpGet("servers")]
124+
public IActionResult GetServerConfig()
125+
{
126+
return Ok(new
127+
{
128+
serverConfig.MaxRetries,
129+
serverConfig.TimeoutSeconds,
130+
EndpointCount = serverConfig.Endpoints.Count,
131+
EnabledEndpoints = serverConfig.Endpoints.Count(e => e.Enabled),
132+
Endpoints = serverConfig.Endpoints.Select(e => new
133+
{
134+
e.Name,
135+
e.Url,
136+
e.Port,
137+
e.Enabled,
138+
e.HealthCheckIntervalSeconds
139+
})
140+
});
141+
}
142+
121143
[HttpGet("all")]
122144
public IActionResult GetAllConfigs()
123145
{
@@ -159,6 +181,11 @@ public IActionResult GetAllConfigs()
159181
migrationConfig.RecordsPerRun,
160182
migrationConfig.BatchSize,
161183
IsValid = migrationConfig.RecordsPerRun == 0 || migrationConfig.RecordsPerRun >= migrationConfig.BatchSize
184+
},
185+
Servers = new
186+
{
187+
EndpointCount = serverConfig.Endpoints.Count,
188+
EnabledEndpoints = serverConfig.Endpoints.Count(e => e.Enabled)
162189
}
163190
});
164191
}

samples/Fox.ConfigKit.Samples.WebApi/Program.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,25 @@
8686
.GreaterThan(c => c.CommandTimeoutSeconds, 0, "Command timeout must be greater than 0")
8787
.ValidateOnStartup();
8888

89+
// Example 8: Collection validation with ValidateEach
90+
builder.Services.AddConfigKit<ServerConfig>("Servers")
91+
.InRange(c => c.MaxRetries, 0, 10, "Max retries must be between 0 and 10")
92+
.InRange(c => c.TimeoutSeconds, 1, 300, "Timeout must be between 1 and 300 seconds")
93+
.ValidateEach(c => c.Endpoints,
94+
itemBuilder => itemBuilder
95+
.NotEmpty(e => e.Name, "Endpoint name is required")
96+
.NotEmpty(e => e.Url, "Endpoint URL is required")
97+
.InRange(e => e.Port, 1, 65535, "Port must be between 1 and 65535")
98+
.InRange(e => e.HealthCheckIntervalSeconds, 5, 3600, "Health check interval must be between 5 and 3600 seconds"),
99+
minCount: 1,
100+
emptyMessage: "At least one endpoint must be configured")
101+
.ValidateEach(c => c.Endpoints.Where(e => e.Enabled),
102+
itemBuilder => itemBuilder
103+
.GreaterThan(e => e.HealthCheckIntervalSeconds, 0, "Enabled endpoints must have positive health check interval"),
104+
minCount: 1,
105+
emptyMessage: "At least one enabled endpoint is required")
106+
.ValidateOnStartup();
107+
89108
var app = builder.Build();
90109

91110
// Configure the HTTP request pipeline.

samples/Fox.ConfigKit.Samples.WebApi/README.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,84 @@ public sealed class MigrationConfig
382382
}
383383
```
384384

385+
### 8. Collection Validation - ValidateEach with LINQ Support
386+
387+
**Demonstrates:** Validating each item in a collection, including LINQ filtering support
388+
389+
```csharp
390+
builder.Services.AddConfigKit<ServerConfig>("Servers")
391+
.InRange(c => c.MaxRetries, 0, 10, "Max retries must be between 0 and 10")
392+
.InRange(c => c.TimeoutSeconds, 1, 300, "Timeout must be between 1 and 300 seconds")
393+
.ValidateEach(c => c.Endpoints,
394+
itemBuilder => itemBuilder
395+
.NotEmpty(e => e.Name, "Endpoint name is required")
396+
.NotEmpty(e => e.Url, "Endpoint URL is required")
397+
.InRange(e => e.Port, 1, 65535, "Port must be between 1 and 65535")
398+
.InRange(e => e.HealthCheckIntervalSeconds, 5, 3600, "Health check interval must be between 5 and 3600 seconds"),
399+
minCount: 1,
400+
emptyMessage: "At least one endpoint must be configured")
401+
.ValidateEach(c => c.Endpoints.Where(e => e.Enabled),
402+
itemBuilder => itemBuilder
403+
.GreaterThan(e => e.HealthCheckIntervalSeconds, 0, "Enabled endpoints must have positive health check interval"),
404+
minCount: 1,
405+
emptyMessage: "At least one enabled endpoint is required")
406+
.ValidateOnStartup();
407+
```
408+
409+
**Configuration class:**
410+
411+
```csharp
412+
public sealed class ServerConfig
413+
{
414+
public List<ServerEndpoint> Endpoints { get; set; } = [];
415+
public int MaxRetries { get; set; }
416+
public int TimeoutSeconds { get; set; }
417+
}
418+
419+
public sealed class ServerEndpoint
420+
{
421+
public string Name { get; set; } = string.Empty;
422+
public string Url { get; set; } = string.Empty;
423+
public int Port { get; set; }
424+
public bool Enabled { get; set; }
425+
public int HealthCheckIntervalSeconds { get; set; }
426+
}
427+
```
428+
429+
**appsettings.json:**
430+
431+
```json
432+
{
433+
"Servers": {
434+
"MaxRetries": 3,
435+
"TimeoutSeconds": 30,
436+
"Endpoints": [
437+
{
438+
"Name": "Primary API",
439+
"Url": "https://api.primary.example.com",
440+
"Port": 443,
441+
"Enabled": true,
442+
"HealthCheckIntervalSeconds": 60
443+
},
444+
{
445+
"Name": "Secondary API",
446+
"Url": "https://api.secondary.example.com",
447+
"Port": 443,
448+
"Enabled": true,
449+
"HealthCheckIntervalSeconds": 120
450+
},
451+
{
452+
"Name": "Backup API",
453+
"Url": "https://api.backup.example.com",
454+
"Port": 8443,
455+
"Enabled": false,
456+
"HealthCheckIntervalSeconds": 0
457+
}
458+
]
459+
}
460+
}
461+
```
462+
385463
## 🌐 API Endpoints
386464

387465
| Endpoint | Description |
@@ -393,6 +471,7 @@ public sealed class MigrationConfig
393471
| `GET /api/configuration/security` | View security configuration |
394472
| `GET /api/configuration/campaign` | View campaign configuration |
395473
| `GET /api/configuration/migration` | View migration configuration with property comparison |
474+
| `GET /api/configuration/servers` | View server endpoints configuration |
396475
| `GET /api/configuration/all` | View summary of all configurations |
397476

398477
## ⚡ Fail-Fast Behavior
@@ -490,6 +569,7 @@ Unhandled exception. Microsoft.Extensions.Options.OptionsValidationException:
490569
| **DateTime Validation** | `Minimum` for campaign dates, `GreaterThan` for future dates |
491570
| **TimeSpan Validation** | `GreaterThan` for positive intervals, `Maximum` for durations |
492571
| **Property Comparison** | `GreaterThanProperty`, `LessThanProperty`, `MinimumProperty`, `MaximumProperty` |
572+
| **Collection Validation** | `ValidateEach` for endpoint lists with LINQ filtering |
493573
| **File System** | `DirectoryExists`, `FileExists` |
494574
| **Network** | `UrlReachable` |
495575
| **Security** | `NoPlainTextSecrets` |

samples/Fox.ConfigKit.Samples.WebApi/appsettings.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,32 @@
5757
"MaxRetryAttempts": 3,
5858
"RetryDelaySeconds": 5,
5959
"CommandTimeoutSeconds": 30
60+
},
61+
"Servers": {
62+
"MaxRetries": 3,
63+
"TimeoutSeconds": 30,
64+
"Endpoints": [
65+
{
66+
"Name": "Primary API",
67+
"Url": "https://api.primary.example.com",
68+
"Port": 443,
69+
"Enabled": true,
70+
"HealthCheckIntervalSeconds": 60
71+
},
72+
{
73+
"Name": "Secondary API",
74+
"Url": "https://api.secondary.example.com",
75+
"Port": 443,
76+
"Enabled": true,
77+
"HealthCheckIntervalSeconds": 120
78+
},
79+
{
80+
"Name": "Backup API",
81+
"Url": "https://api.backup.example.com",
82+
"Port": 8443,
83+
"Enabled": false,
84+
"HealthCheckIntervalSeconds": 0
85+
}
86+
]
6087
}
6188
}

src/Fox.ConfigKit.ResultKit/Fox.ConfigKit.ResultKit.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup Label="Framework and Language">
44
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
5-
<Version>1.1.0</Version>
5+
<Version>1.2.0</Version>
66
<IsPackable>true</IsPackable>
77
<AssemblyName>Fox.ConfigKit.ResultKit</AssemblyName>
88
<RootNamespace>Fox.ConfigKit.ResultKit</RootNamespace>
@@ -19,7 +19,7 @@
1919
<PackageReadmeFile>README.md</PackageReadmeFile>
2020
<PackageOutputPath>$(MSBuildThisFileDirectory)..\..\artifacts</PackageOutputPath>
2121
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
22-
<PackageReleaseNotes>Version 1.1.0: Property-to-property comparison validation support (synchronized with Fox.ConfigKit 1.1.0).</PackageReleaseNotes>
22+
<PackageReleaseNotes>Version 1.2.0: Collection validation support with ValidateEach (synchronized with Fox.ConfigKit 1.2.0).</PackageReleaseNotes>
2323
<PackageLicenseExpression>MIT</PackageLicenseExpression>
2424
<RepositoryUrl>https://github.com/akikari/Fox.ConfigKit</RepositoryUrl>
2525
<PackageProjectUrl>https://github.com/akikari/Fox.ConfigKit</PackageProjectUrl>

src/Fox.ConfigKit/Fox.ConfigKit.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup Label="Framework and Language">
44
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
5-
<Version>1.1.0</Version>
5+
<Version>1.2.0</Version>
66
<IsPackable>true</IsPackable>
77
<AssemblyName>Fox.ConfigKit</AssemblyName>
88
<RootNamespace>Fox.ConfigKit</RootNamespace>
@@ -19,7 +19,7 @@
1919
<PackageReadmeFile>README.md</PackageReadmeFile>
2020
<PackageOutputPath>$(MSBuildThisFileDirectory)..\..\artifacts</PackageOutputPath>
2121
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
22-
<PackageReleaseNotes>Version 1.1.0: Add property-to-property comparison validation (GreaterThanProperty, LessThanProperty, MinimumProperty, MaximumProperty).</PackageReleaseNotes>
22+
<PackageReleaseNotes>Version 1.2.0: Add collection validation with ValidateEach() extension supporting LINQ filtering.</PackageReleaseNotes>
2323
<PackageLicenseExpression>MIT</PackageLicenseExpression>
2424
<RepositoryUrl>https://github.com/akikari/Fox.ConfigKit</RepositoryUrl>
2525
<PackageProjectUrl>https://github.com/akikari/Fox.ConfigKit</PackageProjectUrl>

0 commit comments

Comments
 (0)