Files
chapter-organizer/Core/Validation/ValidationService.cs
T
poprhythm 1c7e704ad3 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
2025-12-13 22:15:16 -05:00

131 lines
5.0 KiB
C#

using Core.Entities;
using System.Reflection;
namespace Core.Validation;
/// <summary>
/// Main service for executing validation rules and generating warnings
/// </summary>
public class ValidationService
{
private readonly ValidationConfiguration _config;
private readonly List<IValidationRule<Student>> _studentRules;
private readonly List<IValidationRule<Team>> _teamRules;
private readonly List<IValidationRule<StudentEventStatistics>> _statisticsRules;
// Lazy static singletons for rule definitions (instantiated once per application lifetime)
private static readonly Lazy<List<IValidationRule<Student>>> _studentRuleDefinitions =
new(() => DiscoverRules<Student>());
private static readonly Lazy<List<IValidationRule<Team>>> _teamRuleDefinitions =
new(() => DiscoverRules<Team>());
private static readonly Lazy<List<IValidationRule<StudentEventStatistics>>> _statisticsRuleDefinitions =
new(() => DiscoverRules<StudentEventStatistics>());
/// <summary>
/// Create a new validation service with the specified configuration
/// </summary>
/// <param name="config">Validation configuration (uses Default if null)</param>
public ValidationService(ValidationConfiguration? config = null)
{
_config = config ?? ValidationConfiguration.Default;
// Use the shared rule definitions (singleton pattern)
_studentRules = _studentRuleDefinitions.Value;
_teamRules = _teamRuleDefinitions.Value;
_statisticsRules = _statisticsRuleDefinitions.Value;
}
/// <summary>
/// Discover all validation rules for a given entity type using reflection
/// </summary>
/// <typeparam name="TEntity">The entity type to find rules for</typeparam>
/// <returns>List of discovered validation rules</returns>
private static List<IValidationRule<TEntity>> DiscoverRules<TEntity>()
{
var ruleType = typeof(IValidationRule<TEntity>);
return Assembly.GetExecutingAssembly()
.GetTypes()
.Where(t => t.IsClass && !t.IsAbstract && ruleType.IsAssignableFrom(t))
.Select(t => (IValidationRule<TEntity>)Activator.CreateInstance(t)!)
.ToList();
}
/// <summary>
/// Validate a student's event rankings
/// </summary>
/// <param name="student">Student to validate</param>
/// <param name="context">Validation context</param>
/// <returns>List of validation warnings</returns>
public List<ValidationWarning> ValidateStudentRankings(Student student, ValidationContext context)
{
return _studentRules
.Where(rule => rule.AppliesTo(context))
.Select(rule => rule.Validate(student, _config))
.Where(warning => warning != null)
.Cast<ValidationWarning>()
.ToList();
}
/// <summary>
/// Validate a team configuration
/// </summary>
/// <param name="team">Team to validate</param>
/// <param name="context">Validation context (defaults to Team)</param>
/// <returns>List of validation warnings</returns>
public List<ValidationWarning> ValidateTeam(Team team, ValidationContext context = ValidationContext.Team)
{
return _teamRules
.Where(rule => rule.AppliesTo(context))
.Select(rule => rule.Validate(team, _config))
.Where(warning => warning != null)
.Cast<ValidationWarning>()
.ToList();
}
/// <summary>
/// Validate student event assignment statistics
/// </summary>
/// <param name="stats">Student statistics to validate</param>
/// <param name="context">Validation context</param>
/// <returns>List of validation warnings</returns>
public List<ValidationWarning> ValidateStudentStatistics(StudentEventStatistics stats, ValidationContext context)
{
return _statisticsRules
.Where(rule => rule.AppliesTo(context))
.Select(rule => rule.Validate(stats, _config))
.Where(warning => warning != null)
.Cast<ValidationWarning>()
.ToList();
}
/// <summary>
/// Validate all students in a collection
/// </summary>
/// <param name="students">Students to validate</param>
/// <param name="context">Validation context</param>
/// <returns>Dictionary mapping each student to their warnings</returns>
public Dictionary<Student, List<ValidationWarning>> ValidateStudents(
IEnumerable<Student> students,
ValidationContext context)
{
return students.ToDictionary(
student => student,
student => ValidateStudentRankings(student, context)
);
}
/// <summary>
/// Validate all teams in a collection
/// </summary>
/// <param name="teams">Teams to validate</param>
/// <returns>Dictionary mapping each team to their warnings</returns>
public Dictionary<Team, List<ValidationWarning>> ValidateTeams(IEnumerable<Team> teams)
{
return teams.ToDictionary(
team => team,
team => ValidateTeam(team)
);
}
}