feat(phase-4): application layer + background workers — 202/202 tests green
Use cases (Marathon.Application/UseCases/): - PullUpcomingEventsUseCase: scrape + persist new events + capture pre-match snapshots - PullLiveOddsUseCase: refresh live snapshots for all stored events - PullResultsUseCase: Phase 4 scaffold; delegates to ScrapeResultsAsync (Phase 3 no-op); Phase 8 will replace with watch-list polling - ExportToExcelUseCase: resolves export dir from StorageOptions, delegates to IExcelExporter ApplicationModule.AddMarathonApplication(IServiceCollection) — no IConfiguration needed. Background workers (Marathon.Infrastructure/Workers/): - UpcomingEventsPoller: Cronos 6-field cron schedule (default every 6 h) - LiveOddsPoller: fixed interval (WorkerOptions.LivePollIntervalSeconds, default 30 s) - ResultsWatchListPoller: scaffold, disabled by default (WorkerOptions.ResultsPollerEnabled=false) All three: exception-swallowing, cancellation-aware, scoped DI via CreateAsyncScope(). InfrastructureModule.AddMarathonInfrastructure(IServiceCollection, IConfiguration): - Composes AddMarathonPersistence + AddMarathonScraping + WorkerOptions + 3 hosted services App.xaml.cs: replace reflection-based TryAddApplicationAndInfrastructure with direct AddMarathonApplication() + AddMarathonInfrastructure(config) calls. Resolved Phase 3 TODO: bind Sports:Basketball:QuarterMode from config in ScrapingModule. appsettings.json: add Workers.LivePollIntervalSeconds, ResultsPollIntervalSeconds, ResultsPollerEnabled; add Sports.Basketball.QuarterMode. Settings.razor + WorkerOptions (UI) + SharedResource.*.resx: surface new Workers fields. Tests: +14 Application use-case tests, +3 Infrastructure worker tests (185 → 202 total).
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using Marathon.Application;
|
||||
using Marathon.Infrastructure;
|
||||
using Marathon.UI.Services;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -73,9 +75,9 @@ public partial class App : System.Windows.Application
|
||||
builder.Services.AddBlazorWebViewDeveloperTools();
|
||||
#endif
|
||||
|
||||
// Compose Application + Infrastructure modules if they exist. Parallel
|
||||
// Phase 2/3/4 work may still be merging these; we degrade gracefully.
|
||||
TryAddApplicationAndInfrastructure(builder.Services, builder.Configuration);
|
||||
// Application use cases + Infrastructure (persistence, scraping, workers).
|
||||
builder.Services.AddMarathonApplication();
|
||||
builder.Services.AddMarathonInfrastructure(builder.Configuration);
|
||||
|
||||
// MainWindow needs the IServiceProvider for BlazorWebView.Services binding.
|
||||
builder.Services.AddSingleton<MainWindow>();
|
||||
@@ -104,50 +106,6 @@ public partial class App : System.Windows.Application
|
||||
? level
|
||||
: Serilog.Events.LogEventLevel.Information;
|
||||
|
||||
/// <summary>
|
||||
/// Best-effort wiring of the Application + Infrastructure DI modules.
|
||||
/// TODO(phase-4): the orchestrator will land a single
|
||||
/// <c>AddMarathonInfrastructure(config)</c> entry point. Until then we use
|
||||
/// reflection to call whichever extension methods exist so partial merges
|
||||
/// don't break compilation of this host.
|
||||
/// </summary>
|
||||
private static void TryAddApplicationAndInfrastructure(IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
TryInvokeExtension(services, configuration, "Marathon.Application.DependencyInjection", "AddMarathonApplication");
|
||||
TryInvokeExtension(services, configuration, "Marathon.Infrastructure.DependencyInjection", "AddMarathonInfrastructure");
|
||||
TryInvokeExtension(services, configuration, "Marathon.Infrastructure.Persistence.PersistenceServiceCollectionExtensions", "AddMarathonPersistence");
|
||||
TryInvokeExtension(services, configuration, "Marathon.Infrastructure.Scraping.ScrapingServiceCollectionExtensions", "AddMarathonScraping");
|
||||
}
|
||||
|
||||
private static void TryInvokeExtension(IServiceCollection services, IConfiguration configuration, string typeName, string methodName)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Probe across all loaded assemblies — project refs cause them to load on startup.
|
||||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
var type = asm.GetType(typeName, throwOnError: false, ignoreCase: false);
|
||||
if (type is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var method = type.GetMethod(methodName, new[] { typeof(IServiceCollection), typeof(IConfiguration) });
|
||||
if (method is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
method.Invoke(null, new object[] { services, configuration });
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Optional module {Type}.{Method} not wired", typeName, methodName);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnExit(ExitEventArgs e)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -19,9 +19,17 @@
|
||||
"RequestTimeoutSeconds": 30
|
||||
},
|
||||
"Workers": {
|
||||
"UpcomingScheduleCron": "0 */5 * * * *",
|
||||
"UpcomingScheduleCron": "0 0 */6 * * *",
|
||||
"LivePollerEnabled": true,
|
||||
"UpcomingPollerEnabled": true
|
||||
"UpcomingPollerEnabled": true,
|
||||
"LivePollIntervalSeconds": 30,
|
||||
"ResultsPollIntervalSeconds": 300,
|
||||
"ResultsPollerEnabled": false
|
||||
},
|
||||
"Sports": {
|
||||
"Basketball": {
|
||||
"QuarterMode": false
|
||||
}
|
||||
},
|
||||
"Storage": {
|
||||
"DatabasePath": "./data/marathon.db",
|
||||
|
||||
Reference in New Issue
Block a user