1c7e704ad3
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
131 lines
5.0 KiB
C#
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)
|
|
);
|
|
}
|
|
}
|