profileShare

rasmusjy / splitapp-backend-modular-monolith

Read-only snapshot

No repository description.

main default branch 418 files Expires Sep 13, 2026, 9:06 AM
README.md 11,880 bytes
1 # SplitApp — Trip Expense Management (Phase 3 — Modular Monolith)
2
3 URL: https://travel.rasmusj.com/
4
5 SplitApp is an ASP.NET Core 10.0 web application for managing group trips and splitting expenses. Users create trips, invite friends, track costs with flexible splitting (equal / equal-subset / exact / percentage), manage budgets, run polls, maintain a wishlist, and settle debts via an optimized algorithm.
6
7 This repo is the **Phase 3 — Modular Monolith** refactor of the project. The whole product lives under [`SplitApp.Modular/`](SplitApp.Modular/): one deployable, three internally isolated modules (**Users**, **Trips**, **Expenses**), MediatR for cross-module communication, schema-per-module Postgres isolation.
8
9 Built for the TalTech "Web Applications with C#" course **Personal Project — Phase 3**.
10
11 ---
12
13 ## Run
14
15 ```bash
16 docker compose up --build
17 ```
18
19 Brings up two containers:
20
21 | Service | Container | Port | Notes |
22 |---|---|---|---|
23 | `phase3` | `phase3` | http://localhost:90 | The web app |
24 | `db` | `phase3-db` | (internal only) | PostgreSQL 16, schemas `users` / `trips` / `expenses` — not exposed to host |
25
26 Module migrations run automatically on host startup. Sample data is seeded if `DataInitialization:SeedData=true` (set in `appsettings.json`).
27
28 Test login (after seed):
29 - `admin@taltech.ee` / `Foo.Bar.1` — `admin` role, full Admin area access
30 - `alice@taltech.ee`, `bob@taltech.ee`, `charlie@taltech.ee`, `diana@taltech.ee` / `Foo.Bar.1` — regular users with sample trips
31
32 Stop:
33
34 ```bash
35 docker compose down
36 ```
37
38 ---
39
40 ## Architecture at a glance
41
42 ```
43 ┌────────────────────────────────────────────────┐
44 │ SplitApp.WebApp (host) │
45 │ Program.cs · Controllers · Areas/Admin │
46 │ Application/{Services, DTO, Mappers, │
47 │ Persistence} │
48 └────────────────────────────────────────────────┘
49 │ │ │
50 ▼ ▼ ▼
51 ┌──────────┐ ┌──────────┐ ┌──────────┐
52 │ Users │ │ Trips │ │ Expenses │
53 │ Domain │ │ Domain │ │ Domain │
54 │ App │ │ App │ │ App │
55 │ Infra │ │ Infra │ │ Infra │
56 │ Api │ │ Api │ │ Api │
57 │ schema: │ │ schema: │ │ schema: │
58 │ users │ │ trips │ │ expenses │
59 └──────────┘ └──────────┘ └──────────┘
60 ▲ ▲ ▲
61 └─MediatR───┴─MediatR───┘
62 ┌──────────────────────┐ ┌──────────────────────┐
63 │ Shared.Contracts │ │ Shared.Kernel │
64 │ IRequest / INotification│ │ BaseEntity, LangStr │
65 └──────────────────────┘ └──────────────────────┘
66 ```
67
68 **Reference rules** (compiler-enforced + verified by `tests/SplitApp.WebApp.IntegrationTests/Architecture/`):
69
70 - A module's `Application` / `Infrastructure` / `Api` can reference: same-module projects + `Shared.Kernel` + `Shared.Contracts`. Nothing else.
71 - Inter-module function calls go through **MediatR only**.
72 - `Shared.*` may not reference any module.
73 - `WebApp` is the only project that references all three modules' `Api` and `Infrastructure`.
74
75 **One caveat at the Domain level only:** to keep view-rendering parity from phase 2 (`Trip.CreatedBy.Email`, `Expense.PaidByUser.FirstName`, etc.), the entity classes still declare cross-module navigation properties — annotated `[NotMapped]` so EF never crosses Postgres schemas. The `CrossModuleNavigationTests` invariant rejects any *mapped* cross-module nav. Application/Infrastructure/Api remain isolated; only entity *types* are shared at the Domain level.
76
77 See [explanation.md](explanation.md) (Estonian, full walkthrough), [arhitektuur.md](arhitektuur.md) (Estonian, diagrams + reference rules), [SplitApp.Modular/docs/ARCHITECTURE.md](SplitApp.Modular/docs/ARCHITECTURE.md) (English, deep-dive).
78
79 ---
80
81 ## Solution layout
82
83 ```
84 SplitApp.Modular/
85 ├── SplitApp.sln
86 ├── Directory.Build.props
87 ├── src/
88 │ ├── SplitApp.WebApp/ ← composition root, host
89 │ │ ├── Program.cs ← AddXxxModule(...) wiring
90 │ │ ├── Application/ ← lifted phase-2 BLL
91 │ │ │ ├── Services/ (+ Admin/, Identity/)
92 │ │ │ ├── DTO/
93 │ │ │ ├── Mappers/
94 │ │ │ ├── Persistence/AppUnitOfWork.cs ← aggregates 3 module DbContexts
95 │ │ │ └── Persistence/CrossModuleNavigationLoader.cs
96 │ │ ├── Areas/Admin/
97 │ │ ├── Areas/Identity/
98 │ │ ├── Controllers/
99 │ │ ├── Views/
100 │ │ └── Resources/ ← i18n .resx (EN + ET)
101 │ ├── Shared/
102 │ │ ├── SplitApp.Shared.Kernel/ ← BaseEntity, IBaseRepo, IUoW, LangStr, IdentityHelpers
103 │ │ └── SplitApp.Shared.Contracts/ ← MediatR IRequest / INotification
104 │ └── Modules/
105 │ ├── Users/ ← AppUser, AppRole, AppRefreshToken; JWT
106 │ ├── Trips/ ← Trip, Participant, Invitation, Poll, Wishlist, BudgetCategory
107 │ └── Expenses/ ← Expense, ExpenseSplit, SettlementPlan/Payment, Currency, SplitPreset
108 └── tests/
109 ├── SplitApp.Modules.Users.Tests/
110 ├── SplitApp.Modules.Trips.Tests/
111 ├── SplitApp.Modules.Expenses.Tests/
112 └── SplitApp.WebApp.IntegrationTests/ ← architecture invariants + smoke
113 ```
114
115 Each module = mini-Clean-Architecture (`Domain` ← `Application` ← `Infrastructure`, `Api` for REST). Each module owns its `DbContext` scoped to its own Postgres schema.
116
117 ---
118
119 ## URL map
120
121 | URL | Purpose |
122 |---|---|
123 | `/` | Landing page (MVC) |
124 | `/Trips`, `/Trips/{Create,Details/{id},Edit/{id},Delete/{id}}` | Trip CRUD |
125 | `/Members?tripId={id}` and `/Members/AcceptInvitation/{token}` | Trip participants + invitation flow |
126 | `/Expenses?tripId={id}` (with Create/Edit/Delete) | Trip expenses |
127 | `/Budget?tripId={id}` (CreateCategory/EditCategory/DeleteCategory) | Budget categories |
128 | `/Settlement?tripId={id}` | Balances + settlement plans |
129 | `/PollsClient?tripId={id}` (Create/Details) | Trip polls |
130 | `/WishlistClient?tripId={id}` | Trip wishlist |
131 | `/Identity/Account/Register` | Cookie register |
132 | `/Admin/Dashboard` | Admin home (`admin` role) |
133 | `/Admin/{Users, Trips, Expenses, BudgetCategories, Currencies, Invitations, Polls, SettlementPlans, SettlementPayments, SplitPresets, TripParticipants, Wishlist}` | Admin CRUD per entity |
134 | `/swagger` | Swagger UI listing every module's REST endpoints |
135
136 ### REST API
137
138 | Module | Endpoints |
139 |---|---|
140 | Users | `/api/v1/identity/account/{register, login, logout, refreshtokendata}` |
141 | Trips | `/api/v1/trips`, `/api/v1/budgetcategories`, `/api/v1/invitations`, `/api/v1/polls`, `/api/v1/wishlist` |
142 | Expenses | `/api/v1/expenses`, `/api/v1/currencies`, `/api/v1/settlements`, `/api/v1/splitpresets` |
143
144 All API endpoints require JWT bearer auth except `account/register` and `account/login`.
145
146 ---
147
148 ## Inter-module communication
149
150 All cross-module calls go through MediatR. Contracts in `Shared.Contracts/<Module>/{Queries|Events|Commands}/`; handlers in the **owning** module.
151
152 | Contract | Owner | Purpose |
153 |---|---|---|
154 | `GetUserByIdQuery → UserDto?` | Users | Display name lookup |
155 | `GetUsersByIdsQuery → IReadOnlyList<UserDto>` | Users | Batch lookup |
156 | `UserDeletedEvent` | Users | Trips + Expenses subscribe to clean up rows |
157 | `GetTripByIdQuery → TripSummaryDto?` | Trips | Cross-module trip lookup |
158 | `GetTripParticipantsQuery → IReadOnlyList<TripParticipantDto>` | Trips | |
159 | `IsTripParticipantQuery → bool` | Trips | **IDOR guard** in `ExpensesController` |
160 | `TripDeletedEvent` | Trips | Expenses subscribes to delete dependent expenses/settlements |
161 | `GetTripExpenseTotalsQuery → TripExpenseTotalsDto` | Expenses | |
162 | `GetBudgetCategorySpentQuery → IReadOnlyDictionary<Guid, decimal>` | Expenses | Per-budget-category spent totals |
163 | `ExpenseSettledEvent` | Expenses | Reserved |
164 | `SettlementPlanCompletedEvent` | Expenses | Trips advances "Finalizing" trips → "Settled" once every payment is confirmed |
165
166 ---
167
168 ## Tests
169
170 ```bash
171 cd SplitApp.Modular
172 dotnet test
173 ```
174
175 **25 tests** across four projects:
176
177 - Per-module unit tests — `CurrencyConverter` (5), `LangStr` (7), `IdentityHelpers` JWT round-trips (4)
178 - Architecture invariants — `ModuleBoundaryTests`, `DbContextSchemaIsolationTests`, `CrossModuleNavigationTests`
179 - Smoke — `WebApplicationFactory<Program>` boots the host, hits `/`, `/Home/Index`, expects 401 on unauthenticated API
180
181 A failing architecture test = someone violated the modular monolith invariant.
182
183 ---
184
185 ## Phase 3 ↔ Phase 2 mapping
186
187 Phase 3 lifts most of phase 2 unchanged:
188
189 | Phase 2 | Phase 3 destination |
190 |---|---|
191 | `Base.Domain`, `Base.Contracts` | `Shared.Kernel` |
192 | `Base.Helpers` (IdentityHelpers) | `Shared.Kernel.Auth` |
193 | `App.Domain.Identity.*` | `Modules/Users/Domain/Entities/` |
194 | `App.Domain.{Trip, TripParticipant, ...}` | `Modules/Trips/Domain/Entities/` |
195 | `App.Domain.{Expense, SettlementPlan, ..., Currency}` | `Modules/Expenses/Domain/Entities/` |
196 | `App.DAL.EF.AppDbContext` | split into 3 per-module `DbContext`s |
197 | `App.BLL.Services.Identity.*` | `Modules/Users/Application/Services/` |
198 | `App.BLL.Services.*` (Trip, Expense, Settlement, ...) | `WebApp/Application/Services/` (composition-root facade over 3 module UoWs) |
199 | `App.BLL.{DTO, Mappers}` | `WebApp/Application/{DTO, Mappers}` |
200 | `WebApp.ApiControllers.{Identity, Trips, Expenses, ...}` | split per module → `Modules/X/Api/Controllers/` |
201 | `WebApp/{Controllers, Areas/Admin, Areas/Identity, Views}` | preserved structurally; namespace re-rooted to `SplitApp.WebApp.*` |
202
203 The phase-2 BLL is "lifted" into `WebApp/Application/` rather than rewritten — this keeps the full UX surface (101 Razor views, 21 MVC + Admin controllers, 10 REST controllers, Identity Razor pages) byte-identical to phase 2 while the inter-module boundaries are enforced cleanly via MediatR + per-module DbContexts.
204
205 ---
206
207 ## Files & docs
208
209 - [explanation.md](explanation.md) — Phase 3 walkthrough (Estonian)
210 - [arhitektuur.md](arhitektuur.md) — Phase 3 architecture diagrams + rules (Estonian)
211 - [architecture.md](architecture.md) — Phase 3 architecture deep-dive (English)
212 - [modularmonolith.md](modularmonolith.md) — Course material on the modular monolith pattern
213 - [phase3.md](phase3.md) — The original assignment text
214 - [SplitApp.Modular/README.md](SplitApp.Modular/README.md) — Module-level README
215 - [SplitApp.Modular/docs/ARCHITECTURE.md](SplitApp.Modular/docs/ARCHITECTURE.md) — Per-module architecture details
216