4.6 KiB
The Biergarten App — Vertical Slice Architecture
Two diagrams, split by concern: which host wires up which slice at startup, and which projects each slice actually compiles against. Mixing DI wiring and compile-time references in one graph is what made the old diagram unreadable.
Host wiring
Each host (API.Core, Database.Seed) decides for itself which Features.*
assemblies to register — there's no shared bootstrap. Database.Migrations
doesn't wire up any slice; it applies schema directly.
--8<-- "web/diagrams/vertical-slice/host-wiring.mmd"
Project reference dependencies
Solid arrows are compile-time project references — every slice-to-slice
dependency is one now that the email MediatR contract has moved off
Shared.Application: Features.Users on Features.PhotoUpload for avatar
uploads, and Features.Emails on Features.Users for the registration and
resend-confirmation notifications. Every DB-backed slice depends on the same
three foundation projects (Shared.Contracts, Domain.Entities,
Database.Connection) — drawn as one box instead of fifteen separate arrows;
see the notes for what else each of those foundation projects pulls in.
--8<-- "web/diagrams/vertical-slice/project-references.mmd"
Notes
-
API.Core: Program.cs wiring only
- MediatR +
AddApplicationPartper HTTP-exposedFeatures.*assembly - Swagger/OpenAPI, health checks, JWT auth middleware, global exception filter
- Only
Features.UsersandFeatures.BreweriesgetAddApplicationPart(controllers) Features.PhotoUploadandFeatures.Emailsscanned for MediatR handlers onlyFeatures.LocationsandFeatures.Beersnot referenced byAPI.Coreat all
- MediatR +
-
Feature slices: each owns its own controller (where present), commands/queries + handlers, validators, and Dapper repository
Features.Userscovers auth (Commands/Authentication), account management (Commands/Account), and profile/avatar (Commands/Profile)Features.EmailsandFeatures.PhotoUploadhave no controller — invoked only via MediatR commands sent from other slicesFeatures.Locationshas no controller; one MediatR handler (GetOrCreateCityHandler) registered byDatabase.Seedonly —API.Corenever scans itFeatures.Beersis scaffolded (project + references only, no source files yet)
-
The exceptions to slice isolation:
Features.Userstakes a direct project reference toFeatures.PhotoUpload- sends its
UploadPhotoCommandvia MediatR fromUploadAvatarHandler - the command type lives in the target slice itself, not in
Shared.Application
- sends its
Features.Emailstakes a direct project reference toFeatures.Users- handles
UserRegisteredNotification(published fromRegisterUserHandler) andConfirmationEmailResendRequestedNotification(published fromResendConfirmationEmailHandler) - both notification types live in
Features.Users(Features.Users.Notifications), not inShared.Application
- handles
-
Shared.Application:
ValidationBehavior(MediatR pipeline)- Every slice (including
Features.Emails) references it for the pipeline behavior alone
-
Domain.Entities:
UserAccount,UserCredential,UserVerification,UserProfile,UserAvatarBreweryPost,BreweryPostLocationCity,StateProvince,CountryBeerPost,BeerStyle,Photo(schema tables exist forBeerPost/BeerStyle; no repository yet sinceFeatures.Beershas no code)
-
Database.Connection: the shared connection-factory project
- each slice's own repository issues inline SQL via Dapper against it
- no ORM, no stored procedures
- Replaces the old
Infrastructure.Sqlproject
-
Database.Migrations: separate console app
schema.dbml+ SQL scripts- applies DDL directly
- distinct from
Database.Seed, which loads data through the feature slices' own DI extensions
-
Database.Seed:
- calls each slice's
AddFeatures*()DI extension directly - builds its own MediatR registration
- no API host, no shared pipeline with
API.Core
- calls each slice's
-
Omitted from the diagram:
Domain.Exceptions(referenced by nearly every project directly, including all ofShared.Contractsand each feature slice)Infrastructure.Configuration(the shared options-binding project referenced byDatabase.Connection,Infrastructure.FileUpload, andFeatures.Emailsdirectly)- both are plumbing-level dependencies of nearly everything above, and drawing them would just add a second copy of the same fan-in