6.0 KiB
6.0 KiB
Phase 1: Solution Skeleton + Domain Model
Status: ⬜ Not Started Parent plan: PLAN.md Domain: backend
Objective
Create the .NET 8 solution structure (5 source projects + 4 test projects) and implement the core domain model — entities, value objects, enums, and invariants — with no external dependencies. This establishes the foundation that all later phases reference.
Tasks
- Create
Marathon.slnwith these projects:src/Marathon.Domain/Marathon.Domain.csproj(classlib, .NET 8, no deps)src/Marathon.Application/Marathon.Application.csproj(classlib, refs Domain)src/Marathon.Infrastructure/Marathon.Infrastructure.csproj(classlib, refs Domain + Application)src/Marathon.UI/Marathon.UI.csproj(Razor Class Library, refs Domain + Application)src/Marathon.Hosts.WpfBlazor/Marathon.Hosts.WpfBlazor.csproj(WPF + BlazorWebView, refs Marathon.UI + Marathon.Infrastructure + Marathon.Application)tests/Marathon.Domain.Tests/Marathon.Domain.Tests.csproj(xUnit)tests/Marathon.Application.Tests/Marathon.Application.Tests.csproj(xUnit)tests/Marathon.Infrastructure.Tests/Marathon.Infrastructure.Tests.csproj(xUnit)tests/Marathon.UI.Tests/Marathon.UI.Tests.csproj(bUnit + xUnit)
- Add
Directory.Build.propsat repo root with shared settings:<Project> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> <LangVersion>12</LangVersion> <TreatWarningsAsErrors Condition="'$(Configuration)'=='Release'">true</TreatWarningsAsErrors> <AnalysisLevel>latest</AnalysisLevel> </PropertyGroup> </Project> - Add
Directory.Packages.propsfor centralized NuGet versions (mark<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>). - Add
.editorconfigat repo root with C# formatting rules consistent with CLAUDE.md conventions (file-scoped namespaces, 4-space indent, etc.). - Implement
Marathon.Domaintypes:- Value objects (records):
SportCode(int Value)— must be > 0EventId(string Value)— bookmaker's event identifier (string, not int)Sideenum:Side1, Side2, Draw, Less, MoreBetScopediscriminated union:Match | Period(int Number)(use record hierarchy)BetTypeenum:Win, Draw, WinFora, TotalOddsRate(decimal Value)— must be > 1.0OddsValue(decimal Value)— handicap or total threshold (e.g., -5.5, 220.5)
- Entities (use records or classes with private setters as appropriate):
Sport(SportCode Code, string NameRu, string NameEn)Country(string Code, string NameRu, string NameEn)League(string Id, SportCode Sport, string Country, string NameRu, string NameEn, string Category)Event(EventId Id, SportCode Sport, string CountryCode, string LeagueId, string Category, DateTimeOffset ScheduledAt, string Side1Name, string Side2Name)Bet(BetScope Scope, BetType Type, Side Side, OddsValue? Value, OddsRate Rate)OddsSnapshot(EventId EventId, DateTimeOffset CapturedAt, OddsSource Source, IReadOnlyList<Bet> Bets)whereOddsSource = PreMatch | LiveEventResult(EventId EventId, int Side1Score, int Side2Score, Side WinnerSide, DateTimeOffset CompletedAt)Anomaly(Guid Id, EventId EventId, DateTimeOffset DetectedAt, AnomalyKind Kind, decimal Score, string EvidenceJson)whereAnomalyKind = SuspensionFlip
- Value objects (records):
- Implement domain invariants in record constructors / static factory methods.
- Implement
Marathon.Domain.Tests— TDD tests for invariants:OddsRaterejects ≤ 1.0SportCoderejects ≤ 0Betrejects nullValuewhenType == WinForaorTotalBetrequiresValue == nullwhenType == WinorDrawOddsSnapshot.Betsis non-emptyEvent.ScheduledAtis UTC (Offset == TimeSpan.Zero)- Domain types are immutable (no settable public properties)
Files to Modify/Create
Marathon.slnDirectory.Build.propsDirectory.Packages.props.editorconfigsrc/Marathon.Domain/**— entities, VOs, enums, invariantssrc/Marathon.Application/Marathon.Application.csproj— empty stub csprojsrc/Marathon.Infrastructure/Marathon.Infrastructure.csproj— empty stubsrc/Marathon.UI/Marathon.UI.csproj— empty RCL stubsrc/Marathon.Hosts.WpfBlazor/Marathon.Hosts.WpfBlazor.csproj— empty stubtests/Marathon.Domain.Tests/**— invariant teststests/Marathon.{Application,Infrastructure,UI}.Tests/*.csproj— empty xUnit stubs
Acceptance Criteria
dotnet build Marathon.slnsucceeds (compile-only smoke check, allowed in Big Bang).- All domain tests pass (
dotnet test tests/Marathon.Domain.Testsis allowed even in Big Bang since this is the foundation phase and the test project is self-contained). - Domain types are public, immutable records with invariants enforced in constructors.
- No EF Core, scraping, or UI code in this phase.
Notes
- Use file-scoped namespaces and one type per file (except small enum + record groups).
- Domain types must NOT reference
System.Net.Http, EF Core, or any infrastructure. - For the discriminated union
BetScope, use a record hierarchy:Or a single record with a nullablepublic abstract record BetScope { /* private ctor */ } public sealed record MatchScope : BetScope; public sealed record PeriodScope(int Number) : BetScope;PeriodNumber— implementer's choice, document it. - Test framework: xUnit with FluentAssertions. Don't add Mockito/NSubstitute yet (no abstractions to mock in Domain).
Review Checklist
- Solution builds (
dotnet build) - Domain tests all pass
- No external deps in
Marathon.Domain.csprojexcept framework packages - Public API surface is minimal — only what later phases need
- All types follow CLAUDE.md naming/style conventions