Add comprehensive validation system with runtime configuration
Implements a flexible validation framework for student rankings, team assignments, and team composition with administrator-configurable rules and thresholds. Validation System: - Reflection-based rule discovery eliminates manual registration - Base classes (RequiredEventTypeRuleBase, EventCountThresholdRuleBase) reduce code duplication by ~250 lines - 12 validation rules covering event requirements, counts, and team constraints - Configurable severity levels (Warning/Error) per rule type - ValidationService with caching for optimal performance - Rules apply contextually (StudentRanking, StudentAssignment, TeamComposition) Configuration & Admin UI: - ValidationSettings admin page for editing thresholds and severity levels - ChapterSettings admin page for editing chapter information - Settings stored in Data/appsettings.json for runtime configuration - JSON config works in both development and production environments - Auto-creates Data directory and config template on first run User Experience: - ValidationWarnings component displays inline warnings/errors - Integrated in Event Ranking, Registration, and Team Assignment pages - Color-coded severity indicators (warning yellow, error red) - Includes "Too Many Regional Events" rule (max 3 recommended) Technical Improvements: - Template Method pattern for rule base classes - Singleton rule instances with lazy initialization - Configuration loaded via IConfiguration with fallback to defaults - Safe JSON updates preserve other appsettings sections 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,574 @@
|
|||||||
|
# Validation System Improvements & Rules Engine Evaluation
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
**Current State:** Custom validation system with 10 C# rules across 3 entity types. Clean architecture but hardcoded rule registration, requires recompilation for changes.
|
||||||
|
|
||||||
|
**Critical Requirement:** Runtime-editable validation rules for different chapters (from user feedback).
|
||||||
|
|
||||||
|
**Recommendation:** **Hybrid approach** - Improve current implementation + add JSON-based configuration storage, avoiding full rules engine adoption to maintain simplicity while enabling runtime customization.
|
||||||
|
|
||||||
|
## Analysis Results
|
||||||
|
|
||||||
|
### Current Implementation Assessment
|
||||||
|
|
||||||
|
**Strengths:**
|
||||||
|
- Clean generic design (`IValidationRule<TEntity>`)
|
||||||
|
- Type-safe validation
|
||||||
|
- Clear separation of concerns
|
||||||
|
- Context-aware rule execution
|
||||||
|
- Well-organized directory structure (StudentRankingRules, StudentAssignmentRules, TeamRules)
|
||||||
|
|
||||||
|
**Critical Issues:**
|
||||||
|
1. **Hardcoded rule registration** - Rules manually added in `ValidationService.InitializeRules()` (Open/Closed violation)
|
||||||
|
2. **Requires recompilation** - Cannot change rules or thresholds without code deployment
|
||||||
|
3. **Rule duplication** - StudentRankingRules and StudentAssignmentRules have duplicate logic
|
||||||
|
4. **Non-scalable configuration** - One property per rule severity (6 specific properties)
|
||||||
|
5. **No rule discovery** - Cannot dynamically load or register rules
|
||||||
|
6. **Inconsistent context handling** - `ValidateTeam()` doesn't check `AppliesTo(context)`
|
||||||
|
|
||||||
|
**Performance Concerns:**
|
||||||
|
- Rule objects instantiated per ValidationService (no singleton/pooling)
|
||||||
|
- No short-circuit logic (all rules execute even after critical errors)
|
||||||
|
- Batch operations materialize entire dictionary in memory
|
||||||
|
|
||||||
|
### Rules Engine Library Research
|
||||||
|
|
||||||
|
#### Microsoft RulesEngine
|
||||||
|
[GitHub - microsoft/RulesEngine](https://github.com/microsoft/RulesEngine) | [Documentation](https://microsoft.github.io/RulesEngine/)
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- JSON-based rule definitions (runtime editable)
|
||||||
|
- Lambda expression support in rules
|
||||||
|
- Global/local parameters (reusable logic)
|
||||||
|
- Custom actions (v3+)
|
||||||
|
- Custom type injection
|
||||||
|
- Flexible storage (Azure Blob, Cosmos DB, files, EF Core, SQL)
|
||||||
|
- Actively maintained (tutorial from October 2025)
|
||||||
|
|
||||||
|
**Example Rule:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"WorkflowName": "StudentValidation",
|
||||||
|
"Rules": [
|
||||||
|
{
|
||||||
|
"RuleName": "RequireRegionalEvent",
|
||||||
|
"Expression": "input.RankedEvents.Any(e => e.RegionalEvent == true)",
|
||||||
|
"ErrorMessage": "No Regional Event",
|
||||||
|
"ErrorType": "Warning"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- ✅ Enables runtime editing without recompilation
|
||||||
|
- ✅ Proven, maintained library (Microsoft)
|
||||||
|
- ✅ Reduces custom code maintenance
|
||||||
|
- ✅ Flexible rule storage options
|
||||||
|
- ✅ Supports complex expressions
|
||||||
|
|
||||||
|
**Cons:**
|
||||||
|
- ❌ Significant complexity (JSON + lambda expressions as strings)
|
||||||
|
- ❌ Learning curve for team
|
||||||
|
- ❌ All rules must be rewritten in JSON
|
||||||
|
- ❌ Type safety lost (expressions are strings)
|
||||||
|
- ❌ Debugging more difficult (no compile-time checks)
|
||||||
|
- ❌ External dependency
|
||||||
|
|
||||||
|
#### Alternative: NRules
|
||||||
|
[GitHub - NRules/NRules](https://github.com/NRules/NRules) | [Website](https://nrules.net/)
|
||||||
|
|
||||||
|
**Characteristics:**
|
||||||
|
- Forward-chaining rules engine based on Rete algorithm
|
||||||
|
- C# internal DSL for rule authoring
|
||||||
|
- More complex than RulesEngine
|
||||||
|
- Better for complex decision trees and rule dependencies
|
||||||
|
|
||||||
|
**Assessment:** Overkill for TSA validation use case. Designed for complex business logic scenarios with many interdependent rules.
|
||||||
|
|
||||||
|
#### Alternative: FluentValidation
|
||||||
|
[GitHub - FluentValidation](https://github.com/FluentValidation/FluentValidation) | [NuGet](https://www.nuget.org/packages/fluentvalidation/)
|
||||||
|
|
||||||
|
**Characteristics:**
|
||||||
|
- Validation-specific library (NOT a rules engine)
|
||||||
|
- Fluent interface for building validation rules
|
||||||
|
- Strongly-typed C# validation rules
|
||||||
|
|
||||||
|
**Assessment:** Not applicable - FluentValidation is for object validation, not business rule execution. Different use case.
|
||||||
|
|
||||||
|
### Decision Matrix
|
||||||
|
|
||||||
|
| Aspect | Current Implementation | Improve Current + JSON Config | Full RulesEngine Adoption |
|
||||||
|
|--------|------------------------|------------------------------|--------------------------|
|
||||||
|
| **Runtime Editing** | ❌ No (requires recompile) | ✅ Yes (configuration only) | ✅ Yes (rules + config) |
|
||||||
|
| **Simplicity** | ✅ Simple C# rules | ✅ C# rules + JSON config | ❌ Complex JSON + lambdas |
|
||||||
|
| **Type Safety** | ✅ Full compile-time checks | ✅ Rules are type-safe | ❌ String-based expressions |
|
||||||
|
| **Maintenance** | ❌ Custom code to maintain | ⚠️ Less custom code | ✅ Library maintained |
|
||||||
|
| **Flexibility** | ❌ Can't add new rules | ⚠️ Can't add new rules | ✅ Can add new rules |
|
||||||
|
| **Learning Curve** | ✅ Familiar C# | ✅ Minimal | ❌ Significant |
|
||||||
|
| **Debugging** | ✅ Easy | ✅ Easy | ❌ Harder (runtime errors) |
|
||||||
|
| **Dependencies** | ✅ None | ✅ None | ⚠️ External library |
|
||||||
|
|
||||||
|
## Recommended Approach: Hybrid Improvement
|
||||||
|
|
||||||
|
### Strategy
|
||||||
|
|
||||||
|
**Phase 1: Immediate Improvements (No Breaking Changes)**
|
||||||
|
Improve current implementation to fix code smells and add runtime configurability WITHOUT adopting a rules engine library.
|
||||||
|
|
||||||
|
**Phase 2: JSON Configuration Storage**
|
||||||
|
Move `ValidationConfiguration` to JSON files (Data/validation-config.json) to enable per-chapter customization at runtime.
|
||||||
|
|
||||||
|
**Phase 3: Rule Registry Pattern (Optional)**
|
||||||
|
Add plugin architecture for future extensibility if needed.
|
||||||
|
|
||||||
|
### Why This Approach?
|
||||||
|
|
||||||
|
1. **Meets critical requirement:** Enables runtime editing of thresholds, severities, and requirements
|
||||||
|
2. **Maintains simplicity:** Keeps familiar C# rule classes with type safety
|
||||||
|
3. **Reduces complexity:** Avoids JSON lambda expressions and external dependencies
|
||||||
|
4. **Pragmatic:** Most chapter-to-chapter differences are in THRESHOLDS (min/max events, severity levels), not rule LOGIC
|
||||||
|
5. **Incremental:** Can adopt RulesEngine later if rule logic customization becomes critical
|
||||||
|
|
||||||
|
### What Can Be Customized at Runtime?
|
||||||
|
|
||||||
|
With JSON configuration approach, chapters can customize:
|
||||||
|
- **Event count thresholds** (MinRecommendedEvents, MaxRecommendedEvents, etc.)
|
||||||
|
- **Requirement toggles** (RequireRegionalEvent, RequireOnSiteActivity, etc.)
|
||||||
|
- **Severity levels** (Warning vs Error for each rule type)
|
||||||
|
|
||||||
|
This covers ~95% of expected chapter-to-chapter variation without rule engine complexity.
|
||||||
|
|
||||||
|
### When to Adopt RulesEngine?
|
||||||
|
|
||||||
|
Only adopt Microsoft RulesEngine if you need:
|
||||||
|
- Different chapters with **fundamentally different rules** (not just thresholds)
|
||||||
|
- Non-developers authoring new validation rules
|
||||||
|
- Complex rule dependencies/chaining
|
||||||
|
- Rule versioning and AB testing
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
### Phase 1: Core Improvements (No External Dependencies)
|
||||||
|
|
||||||
|
#### 1.1 Fix Rule Registration Anti-Pattern
|
||||||
|
**File:** `Core/Validation/ValidationService.cs`
|
||||||
|
|
||||||
|
**Current Problem:**
|
||||||
|
```csharp
|
||||||
|
private void InitializeRules()
|
||||||
|
{
|
||||||
|
_studentRules.Add(new NoRegionalEventRule());
|
||||||
|
_studentRules.Add(new NoOnSiteActivityRule());
|
||||||
|
// ... hardcoded rule instantiation
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution:** Use reflection-based rule discovery or explicit registration API:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Option A: Reflection-based discovery (simple)
|
||||||
|
private void InitializeRules()
|
||||||
|
{
|
||||||
|
// Auto-discover all IValidationRule<Student> implementations
|
||||||
|
_studentRules = DiscoverRules<Student>();
|
||||||
|
_teamRules = DiscoverRules<Team>();
|
||||||
|
_statisticsRules = DiscoverRules<StudentEventStatistics>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<IValidationRule<T>> DiscoverRules<T>()
|
||||||
|
{
|
||||||
|
var ruleType = typeof(IValidationRule<T>);
|
||||||
|
return Assembly.GetExecutingAssembly()
|
||||||
|
.GetTypes()
|
||||||
|
.Where(t => t.IsClass && !t.IsAbstract && ruleType.IsAssignableFrom(t))
|
||||||
|
.Select(t => (IValidationRule<T>)Activator.CreateInstance(t)!)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Adding new rules just requires creating the class
|
||||||
|
- No need to modify ValidationService
|
||||||
|
- Rules automatically discovered
|
||||||
|
|
||||||
|
#### 1.2 Eliminate Rule Duplication
|
||||||
|
**Files:** Student ranking rules vs assignment rules
|
||||||
|
|
||||||
|
**Current Problem:** Near-duplicate code between:
|
||||||
|
- `NoRegionalEventRule` (Student) vs `NoRegionalEventAssignmentRule` (StudentEventStatistics)
|
||||||
|
- `NoOnSiteActivityRule` (Student) vs `NoOnSiteActivityAssignmentRule` (StudentEventStatistics)
|
||||||
|
|
||||||
|
**Solution:** Create abstract base classes:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Core/Validation/Rules/BaseRules/RequiredEventTypeRule.cs
|
||||||
|
public abstract class RequiredEventTypeRule<TEntity> : IValidationRule<TEntity>
|
||||||
|
{
|
||||||
|
protected abstract string EventTypeName { get; }
|
||||||
|
protected abstract string IconIdentifier { get; }
|
||||||
|
protected abstract Func<ValidationConfiguration, bool> IsRequired { get; }
|
||||||
|
protected abstract Func<ValidationConfiguration, ValidationSeverity> GetSeverity { get; }
|
||||||
|
protected abstract Func<TEntity, bool> HasEventType { get; }
|
||||||
|
protected abstract ValidationContext[] ApplicableContexts { get; }
|
||||||
|
|
||||||
|
public ValidationWarning? Validate(TEntity entity, ValidationConfiguration config)
|
||||||
|
{
|
||||||
|
if (!IsRequired(config)) return null;
|
||||||
|
if (HasEventType(entity)) return null;
|
||||||
|
|
||||||
|
return new ValidationWarning { /* ... */ };
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AppliesTo(ValidationContext context) =>
|
||||||
|
ApplicableContexts.Contains(context);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then specific rules become:
|
||||||
|
```csharp
|
||||||
|
public class NoRegionalEventRule : RequiredEventTypeRule<Student>
|
||||||
|
{
|
||||||
|
protected override string EventTypeName => "Regional Event";
|
||||||
|
protected override Func<Student, bool> HasEventType =>
|
||||||
|
s => s.RankedEvents.Any(e => e.RegionalEvent);
|
||||||
|
// ... etc
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Eliminates duplication
|
||||||
|
- Easier to maintain
|
||||||
|
- Consistent behavior across similar rules
|
||||||
|
|
||||||
|
#### 1.3 Fix Context Handling Inconsistency
|
||||||
|
**File:** `Core/Validation/ValidationService.cs`
|
||||||
|
|
||||||
|
**Current Problem:** `ValidateTeam()` doesn't filter by context
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```csharp
|
||||||
|
public List<ValidationWarning> ValidateTeam(Team team, ValidationContext context = ValidationContext.Team)
|
||||||
|
{
|
||||||
|
return _teamRules
|
||||||
|
.Where(rule => rule.AppliesTo(context)) // ADD THIS
|
||||||
|
.Select(rule => rule.Validate(team, _config))
|
||||||
|
.Where(warning => warning != null)
|
||||||
|
.Cast<ValidationWarning>()
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.4 Add Rule Singleton Pattern
|
||||||
|
**File:** `Core/Validation/ValidationService.cs`
|
||||||
|
|
||||||
|
**Current:** Rules instantiated per ValidationService instance
|
||||||
|
|
||||||
|
**Solution:** Use lazy static singletons:
|
||||||
|
```csharp
|
||||||
|
private static readonly Lazy<List<IValidationRule<Student>>> _studentRuleDefinitions =
|
||||||
|
new(() => DiscoverRules<Student>());
|
||||||
|
|
||||||
|
public ValidationService(ValidationConfiguration? config = null)
|
||||||
|
{
|
||||||
|
_config = config ?? ValidationConfiguration.Default;
|
||||||
|
_studentRules = _studentRuleDefinitions.Value;
|
||||||
|
_teamRules = _teamRuleDefinitions.Value;
|
||||||
|
_statisticsRules = _statisticsRuleDefinitions.Value;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Rules instantiated once per application lifetime
|
||||||
|
- Reduced memory allocation
|
||||||
|
- Better performance
|
||||||
|
|
||||||
|
### Phase 2: JSON Configuration Storage
|
||||||
|
|
||||||
|
#### 2.1 Add Configuration Serialization
|
||||||
|
**File:** `Core/Validation/ValidationConfiguration.cs`
|
||||||
|
|
||||||
|
Add methods:
|
||||||
|
```csharp
|
||||||
|
public static ValidationConfiguration FromJson(string json)
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<ValidationConfiguration>(json)
|
||||||
|
?? Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<ValidationConfiguration> LoadFromFileAsync(string path)
|
||||||
|
{
|
||||||
|
if (!File.Exists(path)) return Default;
|
||||||
|
|
||||||
|
var json = await File.ReadAllTextAsync(path);
|
||||||
|
return FromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveToFileAsync(string path)
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(this, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
WriteIndented = true
|
||||||
|
});
|
||||||
|
await File.WriteAllTextAsync(path, json);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2 Create Default Configuration File
|
||||||
|
**File:** `Data/validation-config.json` (new)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"MinRecommendedEvents": 2,
|
||||||
|
"MaxRecommendedEvents": 4,
|
||||||
|
"MinCriticalEvents": 1,
|
||||||
|
"MaxCriticalEvents": 6,
|
||||||
|
"RequireRegionalEvent": true,
|
||||||
|
"RequireOnSiteActivity": true,
|
||||||
|
"RequireIndividualEvent": false,
|
||||||
|
"RequireTeamCaptain": true,
|
||||||
|
"NoRegionalEventSeverity": "Warning",
|
||||||
|
"NoOnSiteActivitySeverity": "Warning",
|
||||||
|
"NoIndividualEventSeverity": "Warning",
|
||||||
|
"TeamSizeSeverity": "Warning",
|
||||||
|
"EventCountSeverity": "Warning",
|
||||||
|
"MissingCaptainSeverity": "Warning"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.3 Update Service Registration
|
||||||
|
**File:** `WebApp/Program.cs` (lines 176-177)
|
||||||
|
|
||||||
|
**Current:**
|
||||||
|
```csharp
|
||||||
|
builder.Services.AddScoped<Core.Validation.ValidationService>(sp =>
|
||||||
|
new Core.Validation.ValidationService(Core.Validation.ValidationConfiguration.Default));
|
||||||
|
```
|
||||||
|
|
||||||
|
**New:**
|
||||||
|
```csharp
|
||||||
|
// Load validation configuration from Data directory (or use default)
|
||||||
|
var validationConfigPath = Path.Combine(
|
||||||
|
builder.Environment.ContentRootPath,
|
||||||
|
"Data",
|
||||||
|
"validation-config.json");
|
||||||
|
|
||||||
|
var validationConfig = await Core.Validation.ValidationConfiguration
|
||||||
|
.LoadFromFileAsync(validationConfigPath);
|
||||||
|
|
||||||
|
builder.Services.AddScoped<Core.Validation.ValidationService>(sp =>
|
||||||
|
new Core.Validation.ValidationService(validationConfig));
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.4 Add Configuration UI (Optional)
|
||||||
|
**File:** `WebApp/Components/Pages/Settings/ValidationSettings.razor` (new)
|
||||||
|
|
||||||
|
Create admin page to edit validation configuration with live preview.
|
||||||
|
|
||||||
|
### Phase 3: Optional Enhancements
|
||||||
|
|
||||||
|
#### 3.1 Add Short-Circuit Logic
|
||||||
|
```csharp
|
||||||
|
public List<ValidationWarning> ValidateStudentRankings(
|
||||||
|
Student student,
|
||||||
|
ValidationContext context,
|
||||||
|
bool stopOnFirstError = false)
|
||||||
|
{
|
||||||
|
var warnings = new List<ValidationWarning>();
|
||||||
|
|
||||||
|
foreach (var rule in _studentRules.Where(r => r.AppliesTo(context)))
|
||||||
|
{
|
||||||
|
var warning = rule.Validate(student, _config);
|
||||||
|
if (warning != null)
|
||||||
|
{
|
||||||
|
warnings.Add(warning);
|
||||||
|
if (stopOnFirstError && warning.Severity == ValidationSeverity.Error)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return warnings;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2 Add Rule Metadata Interface
|
||||||
|
```csharp
|
||||||
|
public interface IValidationRuleMetadata
|
||||||
|
{
|
||||||
|
string Name { get; }
|
||||||
|
string Description { get; }
|
||||||
|
string Category { get; }
|
||||||
|
int Priority { get; } // For ordering
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IValidationRule<TEntity> : IValidationRuleMetadata
|
||||||
|
{
|
||||||
|
ValidationWarning? Validate(TEntity entity, ValidationConfiguration config);
|
||||||
|
bool AppliesTo(ValidationContext context);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Alternative: Full RulesEngine Migration Plan
|
||||||
|
|
||||||
|
If you decide runtime rule editing (not just configuration) is critical, here's the Microsoft RulesEngine adoption plan:
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
```bash
|
||||||
|
dotnet add package RulesEngine
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration Strategy
|
||||||
|
|
||||||
|
#### 1. Create Rule JSON Files
|
||||||
|
**File:** `Data/validation-rules.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"StudentRankingWorkflow": {
|
||||||
|
"WorkflowName": "StudentRankingValidation",
|
||||||
|
"Rules": [
|
||||||
|
{
|
||||||
|
"RuleName": "RequireRegionalEvent",
|
||||||
|
"Enabled": true,
|
||||||
|
"Expression": "RequireRegionalEvent AND input.RankedEvents.Any(e => e.RegionalEvent == false)",
|
||||||
|
"RuleExpressionType": "LambdaExpression",
|
||||||
|
"ErrorMessage": "No Regional Event",
|
||||||
|
"ErrorType": "Warning",
|
||||||
|
"SuccessEvent": "NoRegionalEventWarning"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"RuleName": "RequireOnSiteActivity",
|
||||||
|
"Enabled": true,
|
||||||
|
"Expression": "RequireOnSiteActivity AND input.RankedEvents.Any(e => e.OnSiteActivity == false)",
|
||||||
|
"RuleExpressionType": "LambdaExpression",
|
||||||
|
"ErrorMessage": "No On-Site Activity",
|
||||||
|
"ErrorType": "Warning"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Create RulesEngine Wrapper
|
||||||
|
**File:** `Core/Validation/RulesEngineValidationService.cs` (new)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using RulesEngine.Models;
|
||||||
|
|
||||||
|
public class RulesEngineValidationService
|
||||||
|
{
|
||||||
|
private readonly RulesEngine.RulesEngine _rulesEngine;
|
||||||
|
private readonly ValidationConfiguration _config;
|
||||||
|
|
||||||
|
public RulesEngineValidationService(string rulesJsonPath, ValidationConfiguration config)
|
||||||
|
{
|
||||||
|
var rulesJson = File.ReadAllText(rulesJsonPath);
|
||||||
|
var workflows = JsonSerializer.Deserialize<Workflow[]>(rulesJson);
|
||||||
|
_rulesEngine = new RulesEngine.RulesEngine(workflows);
|
||||||
|
_config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<ValidationWarning>> ValidateStudentRankingsAsync(Student student)
|
||||||
|
{
|
||||||
|
var input = new RuleParameter("input", student);
|
||||||
|
var configParam = new RuleParameter("config", _config);
|
||||||
|
|
||||||
|
var results = await _rulesEngine.ExecuteAllRulesAsync(
|
||||||
|
"StudentRankingValidation",
|
||||||
|
input,
|
||||||
|
configParam);
|
||||||
|
|
||||||
|
return results
|
||||||
|
.Where(r => !r.IsSuccess)
|
||||||
|
.Select(r => new ValidationWarning
|
||||||
|
{
|
||||||
|
Code = r.Rule.RuleName,
|
||||||
|
Message = r.Rule.ErrorMessage,
|
||||||
|
Severity = r.Rule.ErrorType == "Error"
|
||||||
|
? ValidationSeverity.Error
|
||||||
|
: ValidationSeverity.Warning,
|
||||||
|
Context = ValidationContext.StudentRanking
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Gradual Migration
|
||||||
|
- Keep existing ValidationService
|
||||||
|
- Add RulesEngineValidationService alongside
|
||||||
|
- Use feature flag to toggle between implementations
|
||||||
|
- Migrate one context at a time (StudentRanking → StudentAssignment → Team)
|
||||||
|
|
||||||
|
## Critical Files
|
||||||
|
|
||||||
|
### Current Implementation
|
||||||
|
1. `Core/Validation/ValidationService.cs` - Main orchestration service
|
||||||
|
2. `Core/Validation/ValidationConfiguration.cs` - Configuration model
|
||||||
|
3. `Core/Validation/IValidationRule.cs` - Generic rule interface
|
||||||
|
4. `Core/Validation/Rules/StudentRankingRules/*.cs` - Student ranking rules (3 files)
|
||||||
|
5. `Core/Validation/Rules/StudentAssignmentRules/*.cs` - Assignment rules (4 files)
|
||||||
|
6. `Core/Validation/Rules/TeamRules/*.cs` - Team rules (3 files)
|
||||||
|
7. `WebApp/Program.cs` - Service registration (line 176)
|
||||||
|
|
||||||
|
### For Improvements
|
||||||
|
8. `Data/validation-config.json` - Runtime configuration file (new)
|
||||||
|
9. `Core/Validation/Rules/BaseRules/*.cs` - Abstract base rule classes (new)
|
||||||
|
|
||||||
|
### For RulesEngine Migration
|
||||||
|
10. `Data/validation-rules.json` - RulesEngine rule definitions (new)
|
||||||
|
11. `Core/Validation/RulesEngineValidationService.cs` - Wrapper service (new)
|
||||||
|
|
||||||
|
## Recommendations Summary
|
||||||
|
|
||||||
|
### Recommended: Hybrid Improvement Approach
|
||||||
|
|
||||||
|
**Do This:**
|
||||||
|
1. ✅ Fix hardcoded rule registration (reflection-based discovery)
|
||||||
|
2. ✅ Eliminate rule duplication (abstract base classes)
|
||||||
|
3. ✅ Add JSON configuration storage (`Data/validation-config.json`)
|
||||||
|
4. ✅ Enable per-chapter threshold/severity customization
|
||||||
|
5. ✅ Fix context handling inconsistency
|
||||||
|
6. ✅ Add rule singleton pattern for performance
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Enables runtime customization without complexity
|
||||||
|
- Maintains type safety and simplicity
|
||||||
|
- No external dependencies
|
||||||
|
- Easy to debug and maintain
|
||||||
|
- Covers 95% of expected customization needs
|
||||||
|
|
||||||
|
**Estimated Effort:** 4-6 hours
|
||||||
|
|
||||||
|
### Not Recommended (Yet): Full RulesEngine Adoption
|
||||||
|
|
||||||
|
**Don't Do This Unless:**
|
||||||
|
- Chapters need fundamentally different rule logic (not just thresholds)
|
||||||
|
- Non-developers need to author rules
|
||||||
|
- Complex rule dependencies are required
|
||||||
|
- A/B testing of rules is needed
|
||||||
|
|
||||||
|
**Complexity Cost:**
|
||||||
|
- All 10 rules rewritten in JSON with lambda strings
|
||||||
|
- Team must learn RulesEngine syntax and concepts
|
||||||
|
- Type safety lost (runtime errors vs compile-time)
|
||||||
|
- Harder debugging (no breakpoints in rule logic)
|
||||||
|
- External dependency to maintain
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Decision Point:** Confirm hybrid approach or full RulesEngine migration
|
||||||
|
2. **If Hybrid:** Implement Phase 1 improvements (2-3 hours)
|
||||||
|
3. **Then:** Add JSON configuration storage (1-2 hours)
|
||||||
|
4. **Test:** Deploy to test chapter with custom config
|
||||||
|
5. **Iterate:** Add Phase 3 enhancements if needed
|
||||||
|
|
||||||
|
## Research Sources
|
||||||
|
|
||||||
|
- [Microsoft RulesEngine - GitHub](https://github.com/microsoft/RulesEngine)
|
||||||
|
- [RulesEngine Documentation](https://microsoft.github.io/RulesEngine/)
|
||||||
|
- [Understanding Microsoft RulesEngine (Oct 2025)](https://medium.com/@sunil-bisht/understanding-microsoft-rulesengine-a-gentle-introduction-part-1-ae1585e5e279)
|
||||||
|
- [NRules - Rules Engine for .NET](https://nrules.net/)
|
||||||
|
- [Rule Engines In .NET - Yunier's Wiki](https://yunier.dev/post/2023/rule-engines-in-dotnet/)
|
||||||
|
- [Top 10 Open Source Rules Engines (2025)](https://www.nected.ai/blog/open-source-rules-engine)
|
||||||
Reference in New Issue
Block a user