* Integrate Spectre.Console into seed application, use async data loading * Replace stored-procedure repositories with Dapper-based repositories Removes the USP_* stored procedures for Auth, Location, and Brewery data access in favour of services built with Dapper. Adds a new Features.Locations slice and optimistic concurrency support for the Brewery slice. * Add new documentation for Dapper migration/database schema * Update FluentAssertions to most recent open source ver * Update XML doc comment in backend projects * Modify brewery location dto to include denormalized data * - Updated column names from 'Timer' to 'RowVersion' in schema.sql for - Introduced new BeerPost entity and updated BreweryPost and BreweryPostLocation entities to use 'RowVersion' for concurrency. - Modified UserAccount, UserCredential, UserVerification, City, StateProvince, and Country entities to use 'RowVersion' instead of 'Timer'. - Updated tests and command handlers to reflect changes in concurrency token from 'Timer' to 'RowVersion'. - Adjusted BreweryDto and BreweryDtoMapper to include 'RowVersion' and updated mapping logic accordingly. - Ensured all repository methods and interfaces are aligned with the new concurrency handling approach. * updates * Add BeerStyle entity and related properties; refactor BreweryDto to remove BeerPosts * Fix Api.Specs test failures: stale Timer column refs and missing test.user fixture * Remove repository tests, usages of registration dto used in repo * archive old stored procs/functions * update stale docs * Add instructions for generating cryptographically secure base64 secrets on macOS/Linux and Windows
6.5 KiB
Database
SQL Server 2022 schema, migration approach, and the app's SQL error-handling convention. For the vertical-slice code that talks to this schema, see Architecture.
Schema
The full schema lives in one script:
web/backend/Database/Database.Migrations/scripts/01-schema/schema.sql, run by
Database.Migrations (DbUp) on startup. Each table is queried directly by the
owning feature slice's repository (see
Direct SQL via Dapper/ADO.NET).
All tables use a UNIQUEIDENTIFIER primary key defaulting to NEWID(), and
most carry a RowVersion ROWVERSION column used for optimistic concurrency on
updates (UPDATE ... WHERE ... AND RowVersion = @RowVersion).
Tables backed by the API today
| Table | Backed by | Notes |
|---|---|---|
UserAccount |
Domain.Entities.UserAccount, Features.Auth/Features.UserManagement |
Username and Email are unique |
UserCredential |
Domain.Entities.UserCredential, Features.Auth |
Argon2id password hash; IsRevoked/RevokedAt track credential rotation, not JWTs |
UserVerification |
Domain.Entities.UserVerification, Features.Auth |
Written when a user confirms their email |
BreweryPost |
Domain.Entities.BreweryPost, Features.Breweries |
A user-submitted brewery listing |
BreweryPostLocation |
Domain.Entities.BreweryPostLocation, Features.Breweries |
1:1 with BreweryPost; Coordinates is a GEOGRAPHY column, read via a plain Dapper query that selects CONVERT(varbinary(max), Coordinates), since Dapper can't deserialize SQL Server UDTs directly |
City |
Domain.Entities.City, Features.Locations |
Referenced by BreweryPostLocation.CityID; written via ILocationRepository, read via ILocationRepository and IBreweryRepository's location joins, currently written only by Database.Seed |
StateProvince |
Domain.Entities.StateProvince, Features.Locations |
Referenced by City.StateProvinceID; created on demand by ILocationRepository.GetOrCreateCityIdAsync, read via IBreweryRepository's location joins |
Country |
Domain.Entities.Country, Features.Locations |
Referenced by StateProvince.CountryID; created on demand by ILocationRepository.GetOrCreateCityIdAsync, read via IBreweryRepository's location joins |
Tables defined in the schema but not yet wired to a feature slice
These exist so the schema can support planned functionality, but have no repository or seed data yet:
| Table | Purpose (from schema) |
|---|---|
Photo |
A photo uploaded by a user account |
UserAvatar |
Links a UserAccount to a Photo as its avatar |
UserFollow |
One user following another (CannotFollowOwnAccount check constraint) |
BeerStyle |
A beer style/category; has a Domain.Entities.BeerStyle type, but no repository yet |
BeerPost |
A beer listing, linked to a BreweryPost and BeerStyle; ABV/IBU are constrained (0-67, 0-120); has a Domain.Entities.BeerPost type, but no repository yet |
BeerPostPhoto |
Links a BeerPost to a Photo |
BeerPostComment |
A comment + 1-5 Rating on a BeerPost |
BreweryPostPhoto |
Links a BreweryPost to a Photo |
Migrations
Tool: DbUp, run by the Database.Migrations project/container.
Scripts live under scripts/, applied in order and tracked so a script never
re-runs once it has succeeded. Today that's a single 01-schema/schema.sql.
When the schema needs to change, add a new numbered script rather than editing
schema.sql in place — DbUp's tracking is per-script, so editing an
already-applied script means it won't re-run against existing databases.
Data seeding
Database.Seed populates a target database from data produced by the C++
pipeline (see Pipeline README): countries,
state/provinces, and cities (created on demand while resolving each brewery's
location), user accounts, and brewery posts with locations. It only seeds
tables that have a feature slice to write through (see the table above).
Database error handling
-
Optimistic-concurrency conflicts are detected by an
UPDATEaffecting zero rows against theRowVersioncolumn. -
On failure, the repository throws a
Domain.Exceptionstype directly (NotFoundException,ConflictException,ArgumentException, ...).API.Core'sGlobalExceptionFiltermaps these to HTTP status codes (404, 409, 400, ...); an unmappedSqlException(e.g. a genuine connectivity or constraint-violation failure) maps to 503.- One repository-level exception:
Features.Auth'sAuthRepositorydoes catchSqlExceptiondirectly, to detect a duplicate-key race (SQL Server error 2601 or 2627) when two requests try to verify the same account concurrently — seeAuth Repository.IsDuplicateVerificationViolation.
- One repository-level exception: