BuberDinner is a reference application for evaluating and learning the Trellis V3 stack in a realistic dinner-hosting marketplace: a user becomes a host, publishes menus, schedules dinners, guests reserve seats, and attendees review the menu afterward. It is still a Clean Architecture sample, but the current purpose is more specific: show how Trellis 3.0.0-alpha.342 fits across Domain, Application, Infrastructure, API, and tests on .NET 10.
- Trellis Core:
Result<T>, railway-oriented programming (ROP), the closedErrorunion,Aggregate<TId>, andResourceRef. - ASP.NET boundaries:
ToHttpResponseAsync(), RFC-7807 Problem Details, strong ETags, precondition handling, and idempotency middleware. - Typed primitives:
RequiredString<TSelf>,RequiredEnum<TSelf>, typed scalar value-object route constraints such as{hostId:HostId}. - CQRS with source-generated Mediator: NuGet
MediatorplusTrellis.Mediatorpipeline behaviors; this project does not use MediatR. - Validation at the right boundaries: FluentValidation inside domain factories and command-boundary validation via
Trellis.Mediator.FluentValidation. - State machines:
Trellis.StateMachineover Stateless for dinner and reservation lifecycles, with transitions returningResult<T>. - Resource-based authorization:
IAuthorizeResource<TResource>,IIdentifyResource<TResource,TId>, andSharedResourceLoaderByIdfor host-owned resources. - Testing support:
Trellis.TestingFluentAssertions matchers such as.Should().BeSuccess(),.BeFailureOfType<>(), and.Unwrap(). - Service composition:
services.AddTrellis(t => t.Use*(...))viaTrellis.ServiceDefaultsin the API composition root. - Persistence seam: in-memory repositories by default, with the repository abstractions and DTO layer structured for future EF Core/Cosmos implementations.
| Concern | Current choice |
|---|---|
| Runtime | .NET 10, pinned by global.json to SDK 10.0.300 with rollForward: latestFeature. |
| Trellis | 3.0.0-alpha.342 through central package management in Directory.Packages.props. |
| HTTP/API | ASP.NET Core 10 controllers, API versioning, Microsoft.AspNetCore.OpenApi + Scalar API reference, JWT bearer auth. |
| CQRS | NuGet Mediator (Mediator.Abstractions + Mediator.SourceGenerator), not MediatR. |
| Validation | FluentValidation in domain factories and command-boundary validators. |
| Persistence | In-memory repositories by default; Cosmos support remains behind the Persistence=CosmosDb setting and falls back to in-memory for aggregates not yet implemented there. |
| Tests | xUnit, FluentAssertions, Trellis.Testing, and ASP.NET Core integration tests. |
| Package | Role in this repo |
|---|---|
Trellis.Core |
Result<T>, ROP operators, Error, Aggregate<TId>, ResourceRef. |
Trellis.Asp |
ToHttpResponseAsync(), Problem Details, ETags, route constraints, idempotency middleware. |
Trellis.Primitives |
Required value-object bases and scalar VO binding support. |
Trellis.Mediator |
Logging/tracing/exception/domain-event/resource-auth pipeline behaviors. |
Trellis.Mediator.FluentValidation |
Auto-wired IValidator<TCommand> validation behavior. |
Trellis.FluentValidation |
IValidator<T>.ValidateToResult() bridge used inside factories. |
Trellis.StateMachine |
Stateless adapter with LazyStateMachine<,> and FireResult(...). |
Trellis.Http.Abstractions |
Direct dependency of Application for HTTP-shape primitives used by handlers. |
Trellis.Authorization |
Resource authorization contracts and shared resource loader base (pulled in transitively via Trellis.Mediator). |
Trellis.Testing |
FluentAssertions matchers for Result<T>. |
Trellis.ServiceDefaults |
Fluent services.AddTrellis(t => t.Use*(...)) registration. |
Prerequisite: .NET SDK 10.0.300 or a compatible .NET 10 preview SDK.
dotnet builddotnet testdotnet test runs the domain, application, API, and infrastructure test projects. The current suite is 120 tests in the in-memory path (Domain 45, Application 1, Api 74); two Infrastructure live-database socket tests are a known pre-existing failure when no backing database is available.
dotnet run --project Api\src\BuberDinner.Api.csprojThe InMemory launch profile starts the API at https://localhost:7059. Unless Persistence=CosmosDb is set, the app uses in-memory repositories and is ready for local exploration. The OpenAPI documents are served at /openapi/{version}.json and the Scalar API reference UI is hosted at /scalar.
The repository was built up through seven merged PRs. The docs use local showcase numbering for the feature walkthroughs after the migration PR.
| GitHub PR | Theme | What changed | Walkthrough |
|---|---|---|---|
| #26 | FunctionalDDD to Trellis V3 | Migrated to .NET 10, Trellis V3, closed Error union, Result.Ok/Result.Fail, ToHttpResponseAsync(), and AddTrellisBehaviors. |
Docs/MIGRATION_TO_TRELLIS_V3.md |
| #27 | HTTP preconditions and resource auth | Added Host, strong ETags, If-None-Match, If-Match, 428/412 responses, IAuthorizeResource<Host>, and scalar VO route constraints. |
Docs/PR1_ETAG_AND_RESOURCE_AUTH.md |
| #28 | Dinner lifecycle | Added Dinner, a Stateless-backed state machine, transition commands, and domain events: DinnerScheduled, DinnerStarted, DinnerEnded, DinnerCancelled. |
Docs/PR2_DINNER_STATE_MACHINE.md |
| #29 | Cursor pagination | Added Trellis Page<T>, Cursor, PageBuilder.FromOverFetch, paged menu/dinner list endpoints, and Link: rel="next" headers. |
Docs/PR3_PAGINATION.md |
| #30 | Reservations and idempotency | Added Reservation, idempotent reservation creation with Idempotency-Key, replay/fingerprint behavior, and fail-loud related-aggregate loading. |
Docs/PR4_RESERVATIONS_AND_IDEMPOTENCY.md |
| #31 | Reviews and defaults | Added MenuReview, command-boundary FluentValidation, Trellis.Testing examples, and the AddTrellis(t => ...) ServiceDefaults composition builder. |
Docs/PR5_REVIEWS_FLUENTVALIDATION_TESTING_DEFAULTS.md |
| #32 | ROP refactor sweep | Converted load-check-act seams and TryCreate factories to .ToResult().Ensure().BindAsync().TapAsync() and ValidateToResult(...).Map(...) chains. |
SubmitMenuReviewCommandHandler.cs |
The codebase follows Clean Architecture: Domain at the centre, Application around it, then Infrastructure and Api on the outside. Dependencies always point inward — Domain has no outward references, Application references Domain, and Infrastructure + Api implement abstractions defined further inside.
The same picture, as a runtime flow showing how a request travels through the layers and where each Trellis package plugs in:
flowchart TB
Client["HTTP clients<br/>Requests/*.http<br/>OpenAPI UI"]
ApiLayer["Api/src<br/>ASP.NET Core controllers<br/>Trellis.Asp: HTTP responses, Problem Details,<br/>ETags, idempotency, route constraints"]
ApplicationLayer["Application/src<br/>Mediator commands, queries, handlers<br/>Trellis.Mediator behaviors<br/>FluentValidation command validators<br/>resource loaders and event handlers"]
DomainLayer["Domain/src<br/>Aggregates, value objects, events<br/>Result<T>, closed Error union, ROP<br/>Trellis.Primitives and StateMachine"]
InfrastructureLayer["Infrastructure/src<br/>In-memory repositories by default<br/>persistence DTOs and JWT generator<br/>Cosmos seam for future persistence work"]
Tests["Tests + Requests<br/>Domain/Application/Api coverage<br/>wire-level .http examples"]
Trellis["Trellis V3 packages<br/>Core, Asp, Primitives, Mediator,<br/>FluentValidation, StateMachine,<br/>Authorization, Testing, ServiceDefaults"]
Client --> ApiLayer
ApiLayer -->|"dispatches commands/queries"| ApplicationLayer
ApplicationLayer -->|"loads and saves through abstractions"| InfrastructureLayer
ApplicationLayer -->|"uses aggregates and returns Result<T>"| DomainLayer
InfrastructureLayer -->|"reconstructs and persists aggregates"| DomainLayer
ApiLayer -->|"composition root: services.AddTrellis(t => ...)"| Trellis
ApplicationLayer --> Trellis
DomainLayer --> Trellis
Tests -. verifies .-> ApiLayer
Tests -. verifies .-> ApplicationLayer
Tests -. verifies .-> DomainLayer
The dependency direction stays conventional: API depends on Application, Application depends on Domain abstractions and repository interfaces, Infrastructure implements those interfaces, and Domain stays pure C#.
flowchart LR
User["User<br/>Domain/src/User<br/>authentication identity<br/>JWT sub claim source"]
Host["Host<br/>Domain/src/Host<br/>OwnerId: UserId"]
Menu["Menu<br/>Domain/src/Menu<br/>HostId FK<br/>Sections > Items<br/>ETag-versioned"]
Dinner["Dinner<br/>Domain/src/Dinner<br/>HostId + MenuId FKs<br/>Upcoming → InProgress → Ended<br/>or Upcoming → Cancelled"]
Reservation["Reservation<br/>Domain/src/Reservation<br/>DinnerId + GuestUserId FKs<br/>Reserved → Cancelled"]
MenuReview["MenuReview<br/>Domain/src/MenuReview<br/>MenuId + DinnerId + GuestUserId FKs<br/>rating and comment"]
Bill["Bill<br/>scaffolded only<br/>future behavior"]
User -- "owns / cooks as" --> Host
Host -- "owns" --> Menu
Host -- "schedules" --> Dinner
Menu -- "served at" --> Dinner
User -- "reserves as guest" --> Reservation
Dinner -- "has seats" --> Reservation
Reservation -- "attendance gate" --> MenuReview
Dinner -- "must be Ended" --> MenuReview
Menu -- "reviewed by" --> MenuReview
Dinner -. "future billing" .-> Bill
| Aggregate | Lives at | Purpose |
|---|---|---|
User |
Domain/src/User/ |
Authentication identity and JWT sub claim source. |
Host |
Domain/src/Host/ |
A user wearing the "I cook" hat; owns menus and dinners. |
Menu |
Domain/src/Menu/ |
Recipe set a host can serve; hierarchical sections/items; ETag-versioned. |
Dinner |
Domain/src/Dinner/ |
Scheduled occurrence of a menu; state machine plus transition domain events. |
Reservation |
Domain/src/Reservation/ |
A guest's seat claim for a dinner; idempotent creation and cancellable lifecycle. |
MenuReview |
Domain/src/MenuReview/ |
Rating/comment from a guest who actually attended; gated by dinner/reservation state. |
Bill |
Docs/DomainModels/Aggregates.Bill.md |
Scaffolded for future behavior; not implemented as a domain aggregate yet. |
All application endpoints live under the Api/src/2022-12-21/ controller folder (the date is a source-tree namespace, not the wire version) and expose API version 2022-10-01 via [ApiVersion("2022-10-01")]. Pass ?api-version=2022-10-01 on every call. Authentication is version-neutral.
| Area | Endpoint family | What to look for |
|---|---|---|
| Authentication | POST /authentication/register, POST /authentication/login |
Creates User, issues JWTs, maps login failures to structured errors. |
| Hosts | POST /hosts |
Creates a Host for the authenticated user; owner comes from JWT sub. |
| Menus | /hosts/{hostId:HostId}/menus |
Typed route params, create/get/list/update, strong ETags, conditional GET, If-Match updates. |
| Dinners | /hosts/{hostId:HostId}/dinners |
Schedule/list/get plus start, end, and cancel transition endpoints. |
| Reservations | /reservations, /reservations/mine, dinner reservation lists |
Idempotent create, guest-only get/cancel, host-only dinner reservation listing. |
| Reviews | /menu-reviews |
Submit/update/get/list reviews with command-boundary validation and attendance gating. |
Important failure modes are first-class examples, not edge cases hidden in tests: 428 missing precondition, 412 stale ETag, 422 validation or rule-code failures, 404 leak shields, 401/403 auth failures, 304 conditional GET, and idempotency 400/422 responses.
| Pattern | How it appears here | See |
|---|---|---|
| Result + ROP everywhere | Load-check-act seams compose with .ToResult(), .Ensure(), .BindAsync(), and .TapAsync() instead of nested if/throw flows. |
SubmitMenuReviewCommandHandler.cs |
Closed Error union |
NotFound, InvalidInput, Forbidden, AuthenticationRequired, Unexpected, and Conflict flow to RFC-7807 responses through ToHttpResponseAsync(). |
Migration notes |
| ETag preconditions | GET emits strong ETags; update requires If-Match; stale writes return 412 and missing preconditions return 428. |
PR #27 walkthrough |
| Cursor pagination | List endpoints over-fetch pageSize.Applied + 1 and let PageBuilder.FromOverFetch detect whether a next cursor exists. |
PR #29 walkthrough |
| State machines | Dinner and Reservation transitions use LazyStateMachine<TStatus,TTrigger> and FireResult(trigger).Map(...). |
PR #28 walkthrough |
| Idempotency | [Idempotent] reservation creation replays the same 201 for the same key/body and returns 422 for fingerprint mismatches. |
PR #30 walkthrough |
| Leak-shielded 404s | Non-owning callers receive NotFound with detail instead of 403 when existence should not be revealed. |
Reservations, Reviews |
| Command-boundary validation | IValidator<TCommand> classes are discovered by UseFluentValidation(...) and fail before handlers run. |
PR #31 walkthrough |
| ServiceDefaults composition | The API registers Trellis through services.AddTrellis(t => t.UseAsp().UseProblemDetails().UseMediator()...). |
Api/src/DependencyInjection.cs |
| Task | Starting point |
|---|---|
| See the API composition root | Api/src/DependencyInjection.cs |
| See the standard controller pattern | Api/src/2022-12-21/Controllers/MenusController.cs |
| Follow a multi-gate ROP handler | Application/src/MenuReviews/Commands/SubmitMenuReviewCommandHandler.cs |
| Study state-machine aggregates | Domain/src/Dinner/Entities/Dinner.cs, Domain/src/Reservation/Entities/Reservation.cs |
| Study command-boundary validators | Application/src/MenuReviews/Validators/ |
| Study typed value objects | Domain/src/*/ValueObject/ and Domain/src/*/ValueObjects/ |
| Study in-memory persistence | Infrastructure/src/Persistence/Memory/ |
| Study domain tests using Trellis matchers | Domain/tests/MenuReviewTests.cs |
| Read the bundled Trellis API docs | .github/trellis-api-*.md |
Domain/ Pure C#: aggregates, value objects, events, state machines, and domain FluentValidation rules.
Application/ Mediator commands/queries/handlers, resource loaders, validators, event handlers, repository interfaces.
Infrastructure/ In-memory repositories, persistence DTOs, optional Cosmos seam, JWT token generation.
Api/ ASP.NET Core controllers, OpenAPI + Scalar API reference, auth, versioning, and Trellis composition root.
Requests/ 30 .http files that exercise happy paths and failure modes end-to-end.
Docs/ Migration notes, PR walkthroughs, and per-aggregate domain notes.
.github/ Bundled Trellis API reference docs used by the showcase; not application runtime code.
build/ Repository build support.
The Requests/ directory is organized by feature area and can be used with the VS Code REST Client or JetBrains HTTP Client. The files cover the happy path plus important failure modes: 428 missing precondition, 412 stale ETag, 422 validation/rule codes, 404 leak shields, 401/403 auth, 304 conditional GET, and idempotency 400/422 responses.
Run the API with a fresh in-memory store, then execute the requests in dependency order: authentication, host, menu, dinner, reservation, review. The durable automated coverage is the API integration test project, so dotnet test remains the primary verification command — it exercises the same endpoints end-to-end without any manual replay step.
BuberDinner started as a Clean Architecture and DDD tutorial codebase for a home-restaurant idea, inspired by the original REST API following Clean Architecture & DDD tutorial series. This repository has since evolved into an end-to-end Trellis V3 reference application through the seven merged PRs summarized above.