Product Reviews showcase: StrongTypes end to end#1
Merged
Conversation
Spec-first foundation for the StrongTypes showcase: product-terms business requirements, the technical design (stack, domain model, API surface, conventions), eight ADRs covering the pivotal decisions, and a list of assumptions to validate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
slnx solution, central package management, Directory.Build.props with correctness-as-errors editorconfig, ServiceDefaults in its own namespace. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Product/Review/ReviewVote with strong-typed properties stored directly via UseStrongTypes(); custom Rating [NumericWrapper] strong type (1..5) with its own JSON converter and reused EF converter; one handler file per operation returning Result<T, TError> with per-operation error enums; proof-of-loading aggregates (ProductWithReviews, ReviewWithVotes) with their Include chains in CompleteQueries; idempotent startup seeder building the demo catalog through the domain entities. Rating carries two hand-written members (Equals(int)/CompareTo(int)) to work around a NumericWrapper generator gap with non-generic wrappers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
API: feature slices (Catalog, Reviews, Votes, Profile) with strong-typed DTO records, exhaustive error-enum-to-HTTP mapping in controllers, and one infrastructure concern per file (Authentication, Persistence, OpenApi, RateLimits, ErrorHandling, Observability). Swashbuckle carries strong-type constraints via AddStrongTypes(); the custom Rating maps to minimum/maximum. AppHost: Postgres (persistent in dev, throwaway in E2E), Zitadel v4 + Login V2 containers, idempotent provisioning of the PKCE SPA client and two demo reviewers, endpoint/env wiring for the API and the Vite frontend. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SPA with catalog, product detail, review submit/edit/delete, and voting.
All API access goes through an openapi-fetch client typed by schema.d.ts,
generated from the committed openapi.json snapshot; the PATCH edit builds
its body via the three-state Maybe contract ({} clears, {Value} sets,
omitted leaves unchanged). Auth is oidc-client-ts redirect PKCE against
Zitadel; the Vite dev/preview server proxies /api to the backend using
Aspire-injected env.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n suite
Domain tests (13): Rating bounds/ordering properties, the ApplyEdit three-state
Maybe contract, score arithmetic, and rating-summary aggregation, with a shared
generators class re-exporting the Kalicz.StrongTypes.FsCheck arbitraries.
Integration tests (28): one shared Postgres container + API host per run,
exercising the real migrate/seed startup path; wire-level JsonElement asserts
on anonymous payloads; JWTs minted against a test-owned key through the real
JwtBearer pipeline; covers catalog, filtering/sorting/paging, submit/edit/
delete (incl. the {}/{"Value"} Maybe semantics), voting rules, profile, and
the OpenAPI document constraints. No mocks anywhere.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Unit (16): the buildEditReviewBody three-state Maybe contract, StarRating display/input behavior, ReviewCard vote/ownership rules, problem message extraction. E2E: Playwright boots the full Aspire stack in E2E mode (throwaway containers, preview build on :4173), signs in through the real Zitadel hosted login, and walks browse -> review -> edit (clear pros) -> vote -> delete; a contract spec fails when the committed openapi.json drifts from the running API (refresh via npm run refresh:api). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ntions Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…aims - Guard review reloads with a request sequence so rapid sort/filter changes cannot apply out of order. - E2E builds the frontend inside the AppHost resource (serve:e2e): VITE_* values bake into the bundle at build time, so the build must run with the Aspire-injected env; Playwright no longer pre-builds. - E2E uses a throwaway Zitadel machine-key dir — a PAT left over from a previous run belongs to a dead instance and provisioning would 401. - Rename the zitadeldb resource (collided with the zitadel container name). - Profile claims: the SPA gets name via idTokenUserinfoAssertion; the API enriches the validated principal from the userinfo endpoint (cached per subject) because Zitadel JWT access tokens carry only the subject. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
During a cold first init the PAT file on disk can briefly belong to a previous instance (Zitadel overwrites it only when init finishes) — a 401 means "re-read and try again", not "give up". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI mirrors the house pattern: backend build + tests (analyzers fail the build, Testcontainers integration suite), frontend format/typecheck/unit/ build, and a full-stack Playwright E2E job with cached browsers and trace upload. Dependabot covers nuget (central package management), npm, and github-actions, grouped weekly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The complete showcase + starter template described in the spec docs: a product-reviews platform where every constrained value is a strong type from the request DTO through the domain and PostgreSQL, out into the OpenAPI document, and into the generated TypeScript client.
What is in here
docs/business-requirements.md(product terms),docs/technical-requirements.md(architecture + conventions), 8 ADRs (decisions per aspect, immutable once accepted),docs/assumptions.md(everything the brief left open — please review this file first).Result<T, TError>with per-operation error enums, EF Core storing strong types directly (UseStrongTypes()), proof-of-loading aggregates, idempotent startup seeding.Maybe<T>?three-state PATCH edit; consumer-defined[NumericWrapper] Rating(1–5); Swashbuckle document carriesformat: email,minLength,exclusiveMinimum, and the rating bounds.demo@productreviews.local/critic@productreviews.local), API, and the Vue 3 frontend.openapi-fetchclient generated from the committedopenapi.json; drift fails an E2E contract test.Notes for review
docs/assumptions.mdlists the judgment calls (Vue+Vite over Nuxt, moderation dropped with Temporal, hard delete, pros/cons fields added for the Maybe demo, own-review voting enforced server-side).[NumericWrapper]on a non-generic struct emitsEquals(int?)/CompareTo(int?), which fail the generatedIEquatable<int>/IComparable<int>—Rating.cscarries a two-line workaround; fix belongs in the library repo.🤖 Generated with Claude Code