Files
maraphon-app/plans/initial-implementation/phase-4-application-and-workers.md
T

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 — runs PullUpcomingEventsUseCase on a configurable cron-like schedule (default: every 6 hours)
    • LiveOddsPoller : BackgroundService — runs PullLiveOddsUseCase every Scraping:PollingIntervalSeconds seconds
    • Both honor CancellationToken, log via ILogger<T>, and skip cycles gracefully on errors (don't crash the host)
  • Add WorkerOptions POCO bound to Workers:* config:
    public 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;
    }
    
    Use Cronos package or simple TimeSpan for upcoming schedule.
  • Add DI extension AddMarathonApplication(IServiceCollection, IConfiguration) in Marathon.Application/DependencyInjection.cs:
    • Registers all use cases
  • Update Marathon.Infrastructure/DependencyInjection.cs to also register BackgroundServices under services.AddHostedService<T>().
  • Tests in Marathon.Application.Tests:
    • Mock IOddsScraper + repos with NSubstitute
    • Test: PullUpcomingEventsUseCase persists new events, skips duplicates
    • Test: PullLiveOddsUseCase writes a snapshot per live event
    • Test: PullResultsUseCase respects selection filter (when null, fetches all)
    • Test: ExportToExcelUseCase invokes IExcelExporter.ExportAsync with correct date range
  • Tests in Marathon.Infrastructure.Tests/Workers/:
    • Test: LiveOddsPoller invokes use case at configured interval (use FakeTimeProvider)
    • Test: poller continues after a use-case exception (logs, doesn't propagate)

Files to Modify/Create

  • src/Marathon.Application/UseCases/*.cs
  • src/Marathon.Application/DependencyInjection.cs
  • src/Marathon.Infrastructure/Workers/UpcomingEventsPoller.cs
  • src/Marathon.Infrastructure/Workers/LiveOddsPoller.cs
  • src/Marathon.Infrastructure/Configuration/WorkerOptions.cs
  • tests/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 IHostedService from Microsoft.Extensions.Hosting — works in WPF host via Host.CreateApplicationBuilder() pattern (Phase 5 will expose this).
  • For the cron-style upcoming poller, prefer the Cronos package (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

Handoff to Next Phase