The web layer has no seam between HTTP and behaviour. src/Ssabba.Web/Endpoints/MatchEndpoints.cs
is 256 lines holding both the route mapping and MatchQueries, a static class that takes a
SsabbaDbContext and does the reading, the writing and the rating application. Nothing is
registered in DI beyond IDbContextFactory<SsabbaDbContext>, so there is no unit of behaviour to
name, to reuse, or to exercise without a database.
Standing up the test tiers (#37) made this concrete: every test of match behaviour has to go through
a container and an HTTP request, because there is nothing smaller to call.
This is not a request for indirection everywhere — the rule stays "real collaborators over doubles"
(#37). It is about giving the behaviour a shape.
Target shape
- Models — entities stay classes in
Ssabba.Domain.Entities; DTOs stay records in
Ssabba.Shared. Unchanged, already right.
- Services —
MatchService : IMatchService in a new src/Ssabba.Web/Services/ (or
Ssabba.Application if it grows past the web host), taking IDbContextFactory<SsabbaDbContext>
by constructor injection. MatchQueries.ListAsync / CreateAsync / ListTeamsAsync move onto it
unchanged in behaviour.
- Interfaces — one per service, declared next to it, registered with
services.AddScoped<IMatchService, MatchService>() in a AddSsabbaServices extension mirroring
AddSsabbaInfrastructure.
- Endpoints —
MatchEndpoints shrinks to routing: take IMatchService, call it, map the result
to a status code. No EF types in the file.
- Components — server-rendered pages inject
IMatchService rather than the context factory, so
the CLAUDE.md rule ("server-rendered components and the API return identical results") is enforced
by construction instead of by discipline.
Tasks
Follows #37. Worth doing before #21/#24 add more behaviour to the same file.
The web layer has no seam between HTTP and behaviour.
src/Ssabba.Web/Endpoints/MatchEndpoints.csis 256 lines holding both the route mapping and
MatchQueries, a static class that takes aSsabbaDbContextand does the reading, the writing and the rating application. Nothing isregistered in DI beyond
IDbContextFactory<SsabbaDbContext>, so there is no unit of behaviour toname, to reuse, or to exercise without a database.
Standing up the test tiers (#37) made this concrete: every test of match behaviour has to go through
a container and an HTTP request, because there is nothing smaller to call.
This is not a request for indirection everywhere — the rule stays "real collaborators over doubles"
(#37). It is about giving the behaviour a shape.
Target shape
Ssabba.Domain.Entities; DTOs stayrecords inSsabba.Shared. Unchanged, already right.MatchService : IMatchServicein a newsrc/Ssabba.Web/Services/(orSsabba.Applicationif it grows past the web host), takingIDbContextFactory<SsabbaDbContext>by constructor injection.
MatchQueries.ListAsync/CreateAsync/ListTeamsAsyncmove onto itunchanged in behaviour.
services.AddScoped<IMatchService, MatchService>()in aAddSsabbaServicesextension mirroringAddSsabbaInfrastructure.MatchEndpointsshrinks to routing: takeIMatchService, call it, map the resultto a status code. No EF types in the file.
IMatchServicerather than the context factory, sothe CLAUDE.md rule ("server-rendered components and the API return identical results") is enforced
by construction instead of by discipline.
Tasks
src/Ssabba.Web/Services/withIMatchService/MatchServiceand anAddSsabbaServicesregistration extension
MatchQueriesontoMatchServicewithout behaviour change; delete the static classMatchEndpoints.csto mapping onlyComponents/PagesatIMatchServiceMatchQueriesFollows #37. Worth doing before #21/#24 add more behaviour to the same file.