Files
chapter-organizer/VALIDATION-IMPROVEMENTS-PLAN.md
T
poprhythm 73ad730b38 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>
2025-12-13 22:15:26 -05:00

19 KiB

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 | Documentation

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:

{
  "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 | Website

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 | NuGet

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

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:

private void InitializeRules()
{
    _studentRules.Add(new NoRegionalEventRule());
    _studentRules.Add(new NoOnSiteActivityRule());
    // ... hardcoded rule instantiation
}

Solution: Use reflection-based rule discovery or explicit registration API:

// 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:

// 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:

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:

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:

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:

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)

{
  "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:

builder.Services.AddScoped<Core.Validation.ValidationService>(sp =>
    new Core.Validation.ValidationService(Core.Validation.ValidationConfiguration.Default));

New:

// 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

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

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

dotnet add package RulesEngine

Migration Strategy

1. Create Rule JSON Files

File: Data/validation-rules.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)

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

  1. Data/validation-config.json - Runtime configuration file (new)
  2. Core/Validation/Rules/BaseRules/*.cs - Abstract base rule classes (new)

For RulesEngine Migration

  1. Data/validation-rules.json - RulesEngine rule definitions (new)
  2. Core/Validation/RulesEngineValidationService.cs - Wrapper service (new)

Recommendations Summary

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

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