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:
+53
-40
@@ -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>();
|
||||
|
||||
Reference in New Issue
Block a user