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
This commit is contained in:
2025-12-13 22:15:16 -05:00
parent 215b9dccca
commit 1c7e704ad3
36 changed files with 1839 additions and 83 deletions
+53 -40
View File
@@ -11,62 +11,66 @@ using WebApp.Logging;
var builder = WebApplication.CreateBuilder(args);
// Load optional appsettings from Data directory in production (overrides defaults)
// In development, use appsettings.Development.json instead
if (builder.Environment.IsProduction())
// Load appsettings from Data directory (works in both development and production)
// This allows runtime configuration changes without touching the codebase
var dataAppSettingsPath = Path.Combine(builder.Environment.ContentRootPath, "Data", "appsettings.json");
if (!File.Exists(dataAppSettingsPath))
{
var dataAppSettingsPath = Path.Combine(builder.Environment.ContentRootPath, "Data", "appsettings.json");
if (!File.Exists(dataAppSettingsPath))
Console.WriteLine($"appsettings.json not found at {dataAppSettingsPath}. Creating from template...");
try
{
Console.WriteLine($"appsettings.json not found at {dataAppSettingsPath}. Creating from template...");
// Ensure Data directory exists
Directory.CreateDirectory(Path.Combine(builder.Environment.ContentRootPath, "Data"));
try
// Copy ChapterSettings and ValidationSettings from the base appsettings.json
var baseAppSettingsPath = Path.Combine(builder.Environment.ContentRootPath, "appsettings.json");
if (File.Exists(baseAppSettingsPath))
{
// Ensure Data directory exists
Directory.CreateDirectory(Path.Combine(builder.Environment.ContentRootPath, "Data"));
var baseConfig = File.ReadAllText(baseAppSettingsPath);
var baseDoc = JsonDocument.Parse(baseConfig);
// Copy ChapterSettings from the base appsettings.json
var baseAppSettingsPath = Path.Combine(builder.Environment.ContentRootPath, "appsettings.json");
if (File.Exists(baseAppSettingsPath))
var templateSettings = new Dictionary<string, object?>();
if (baseDoc.RootElement.TryGetProperty("ChapterSettings", out var chapterSettings))
{
var baseConfig = File.ReadAllText(baseAppSettingsPath);
var baseDoc = JsonDocument.Parse(baseConfig);
templateSettings["ChapterSettings"] = JsonSerializer.Deserialize<object>(chapterSettings.GetRawText());
}
if (baseDoc.RootElement.TryGetProperty("ChapterSettings", out var chapterSettings))
if (baseDoc.RootElement.TryGetProperty("ValidationSettings", out var validationSettings))
{
templateSettings["ValidationSettings"] = JsonSerializer.Deserialize<object>(validationSettings.GetRawText());
}
if (templateSettings.Any())
{
var json = JsonSerializer.Serialize(templateSettings, new JsonSerializerOptions
{
var templateSettings = new
{
ChapterSettings = JsonSerializer.Deserialize<object>(chapterSettings.GetRawText())
};
WriteIndented = true
});
var json = JsonSerializer.Serialize(templateSettings, new JsonSerializerOptions
{
WriteIndented = true
});
File.WriteAllText(dataAppSettingsPath, json);
Console.WriteLine("appsettings.json created in Data directory. Please update ChapterSettings with your chapter information.");
}
else
{
Console.WriteLine("WARNING: ChapterSettings not found in base appsettings.json");
}
File.WriteAllText(dataAppSettingsPath, json);
Console.WriteLine("appsettings.json created in Data directory.");
}
else
{
Console.WriteLine("WARNING: No settings to copy from base appsettings.json");
}
}
catch (Exception ex)
{
Console.WriteLine($"WARNING: Unable to create appsettings.json: {ex.Message}");
}
}
// Add Data/appsettings.json as additional configuration source
if (File.Exists(dataAppSettingsPath))
catch (Exception ex)
{
builder.Configuration.AddJsonFile(dataAppSettingsPath, optional: true, reloadOnChange: true);
Console.WriteLine($"Loaded configuration from {dataAppSettingsPath}");
Console.WriteLine($"WARNING: Unable to create appsettings.json: {ex.Message}");
}
}
// Add Data/appsettings.json as additional configuration source (all environments)
if (File.Exists(dataAppSettingsPath))
{
builder.Configuration.AddJsonFile(dataAppSettingsPath, optional: true, reloadOnChange: true);
Console.WriteLine($"Loaded configuration from {dataAppSettingsPath}");
}
// Configure Serilog with custom handling for antiforgery errors
builder.Host.UseSerilog((context, configuration) =>
{
@@ -172,6 +176,15 @@ builder.Services.AddScoped<WebApp.LocalStorageService>();
// State container for maintaining state per user connection (Blazor Server)
builder.Services.AddScoped<StateContainer>();
// Load validation configuration from appsettings (or use default if not found)
var validationConfig = builder.Configuration
.GetSection("ValidationSettings")
.Get<Core.Validation.ValidationConfiguration>() ?? Core.Validation.ValidationConfiguration.Default;
// Validation service with loaded configuration
builder.Services.AddScoped<Core.Validation.ValidationService>(sp =>
new Core.Validation.ValidationService(validationConfig));
// Add authentication services
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<AuthenticationService>();