4.3 KiB
4.3 KiB
Phase 4: Application Layer + Background Workers
Status: ⬜ Not Started Parent plan: PLAN.md Domain: backend Depends on: Phase 1 (Domain), Phase 2 (Storage), Phase 3 (Scraping)
Objective
Wire scraping + storage together via use-case orchestrators in the Application layer and background services that execute pollers on configurable intervals.
Tasks
- Implement use cases in
Marathon.Application/UseCases/:PullUpcomingEventsUseCase(IOddsScraper, IEventRepository, ISnapshotRepository)ExecuteAsync(CancellationToken)→ fetch upcoming events, persist new ones, capture initial pre-match snapshots for each
PullLiveOddsUseCase(IOddsScraper, IEventRepository, ISnapshotRepository)ExecuteAsync(CancellationToken)→ for each currently-live event, fetch a fresh snapshot, persist it
PullResultsUseCase(IOddsScraper, IEventRepository, IResultRepository)ExecuteAsync(DateRange range, IReadOnlyList<EventId>? selection, CancellationToken)→ fetch results for completed events (all or selected)
ExportToExcelUseCase(IExcelExporter, IEventRepository)ExecuteAsync(DateRange, ExportKind, CancellationToken)
- Implement background services in
Marathon.Infrastructure/Workers/:UpcomingEventsPoller : BackgroundService— runsPullUpcomingEventsUseCaseon a configurable cron-like schedule (default: every 6 hours)LiveOddsPoller : BackgroundService— runsPullLiveOddsUseCaseeveryScraping:PollingIntervalSecondsseconds- Both honor
CancellationToken, log viaILogger<T>, and skip cycles gracefully on errors (don't crash the host)
- Add
WorkerOptionsPOCO bound toWorkers:*config:Usepublic sealed class WorkerOptions { public string UpcomingScheduleCron { get; init; } = "0 0 */6 * * *"; // every 6h public bool LivePollerEnabled { get; init; } = true; public bool UpcomingPollerEnabled { get; init; } = true; }Cronospackage or simple TimeSpan for upcoming schedule. - Add DI extension
AddMarathonApplication(IServiceCollection, IConfiguration)inMarathon.Application/DependencyInjection.cs:- Registers all use cases
- Update
Marathon.Infrastructure/DependencyInjection.csto also registerBackgroundServices underservices.AddHostedService<T>(). - Tests in
Marathon.Application.Tests:- Mock
IOddsScraper+ repos with NSubstitute - Test:
PullUpcomingEventsUseCasepersists new events, skips duplicates - Test:
PullLiveOddsUseCasewrites a snapshot per live event - Test:
PullResultsUseCaserespectsselectionfilter (when null, fetches all) - Test:
ExportToExcelUseCaseinvokesIExcelExporter.ExportAsyncwith correct date range
- Mock
- Tests in
Marathon.Infrastructure.Tests/Workers/:- Test:
LiveOddsPollerinvokes use case at configured interval (use FakeTimeProvider) - Test: poller continues after a use-case exception (logs, doesn't propagate)
- Test:
Files to Modify/Create
src/Marathon.Application/UseCases/*.cssrc/Marathon.Application/DependencyInjection.cssrc/Marathon.Infrastructure/Workers/UpcomingEventsPoller.cssrc/Marathon.Infrastructure/Workers/LiveOddsPoller.cssrc/Marathon.Infrastructure/Configuration/WorkerOptions.cstests/Marathon.Application.Tests/UseCases/**tests/Marathon.Infrastructure.Tests/Workers/**
Acceptance Criteria
- Compiles (Big Bang).
- Use cases depend only on Application abstractions (no Infrastructure refs).
- Workers honor cancellation and don't crash on transient errors.
- All variable timing/enabling is configurable.
Notes
- Use
IHostedServicefromMicrosoft.Extensions.Hosting— works in WPF host viaHost.CreateApplicationBuilder()pattern (Phase 5 will expose this). - For the cron-style upcoming poller, prefer the
Cronospackage (small, mature) over hand-rolled scheduling. - Big Bang: compile-only smoke check.
Review Checklist
- Use cases have no Infrastructure dependencies
- Both pollers configurable (interval, enable/disable)
- Cancellation propagated correctly
- Errors logged, not propagated out of
ExecuteAsync