profileShare

rasmusjy / splitapp-backend-modular-monolith

Read-only snapshot

No repository description.

main default branch 418 files Expires Sep 13, 2026, 9:06 AM
explanation.md 20,059 bytes
1 # SplitApp — Reisikulude haldamise rakendus (Phase 3 — Modulaarne Monoliit)
2
3 ## Ülevaade
4
5 SplitApp on ASP.NET Core 10.0 veebirakendus grupireisi kulude jagamiseks ja haldamiseks. Kasutajad loovad reise, kutsuvad sõpru, lisavad kulusid paindliku jagamisega, haldavad eelarvet, peavad küsitlusi, soovinimekirju ja arveldavad võlgu optimeeritud algoritmiga.
6
7 Phase 3 refaktorib Phase 2 Clean/Onion monoliidi **modulaarseks monoliidiks**: üks deployable, kolm sisemiselt isoleeritud moodulit (Users, Trips, Expenses), MediatR moodulite vaheliseks suhtluseks, schema-per-moodul Postgres-i isolatsioon.
8
9 Projekt on tehtud TalTech kursuse "Web Applications with C#" **Personal Project — Phase 3** raames.
10
11 ---
12
13 ## 0. Phase 3 nõuete täitmine
14
15 `phase3.md` ütleb:
16 > *Implement your project in aspnet.core in modular monolith architecture (make copy of phase2, new repo). Split out into at least 3 modules (users, 2 of your own). Use mediator for communication between modules. No direct references between modules.*
17
18 | # | Nõue | Staatus | Asukoht |
19 |---|------|---------|---------|
20 | 1 | ASP.NET Core modulaarne monoliit | ✅ | Kogu [`SplitApp.Modular/`](SplitApp.Modular/) — üks `WebApp` host, 3 moodulit |
21 | 2 | Vähemalt 3 moodulit (users + 2 omad) | ✅ | **Users**, **Trips**, **Expenses** [`SplitApp.Modular/src/Modules/`](SplitApp.Modular/src/Modules/) |
22 | 3 | MediatR moodulite-vaheliseks suhtluseks | ✅ | 11 lepingut [`Shared.Contracts/`](SplitApp.Modular/src/Shared/SplitApp.Shared.Contracts/) — `GetUserByIdQuery`, `TripDeletedEvent`, `IsTripParticipantQuery`, `SettlementPlanCompletedEvent` jne |
23 | 4 | Mitte mingeid otseseid viiteid moodulite vahel | ⚠️ Application/Infrastructure/Api kihil ✅, Domain kihil **kõrvalekalle** | Vaata [§3](#3-mooduli-piirid--viidete-reeglid) |
24
25 Phase 2 nõuded on kõik säilitatud Phase 3-s:
26 - 19 entiteeti (3 Users + 9 Trips + 7 Expenses)
27 - REST API + versioneerimine + Swagger
28 - JWT auth (`Shared.Kernel.Auth.IdentityHelpers`)
29 - Klient-MVC + Admin Area + Identity Razor Pages
30 - i18n UI (resx) + i18n DB (LangStr)
31 - IDOR (kasutaja näeb ainult oma andmeid REST-is — `IsTripParticipantQuery` MediatR-i kaudu)
32 - CI/CD deploy (Dockerfile + docker-compose)
33 - Repositories + UoW + Services + BLL + Mappers (lifted phase 2 BLL `WebApp/Application/`-i)
34
35 ---
36
37 ## 1. Arhitektuur — Modulaarne Monoliit
38
39 ```
40 ┌────────────────────────────────────────────────────┐
41 │ SplitApp.WebApp (Composition Root) │
42 │ Program.cs · Controllers · Areas/Admin · Views │
43 │ Application/{Services, DTO, Mappers, Persistence}│
44 └────────────────────────────────────────────────────┘
45 │ │ │
46 ▼ ▼ ▼
47 ┌─────────┐ ┌─────────┐ ┌─────────┐
48 │ Users │ │ Trips │ │ Expenses│
49 │ Domain │ │ Domain │ │ Domain │
50 │ App │ │ App │ │ App │
51 │ Infra │ │ Infra │ │ Infra │
52 │ Api │ │ Api │ │ Api │
53 │ schema: │ │ schema: │ │ schema: │
54 │ users │ │ trips │ │ expenses│
55 └─────────┘ └─────────┘ └─────────┘
56 ▲ ▲ ▲
57 └─MediatR───┴─MediatR───┘
58 ┌──────────────────────┐ ┌──────────────────────┐
59 │ Shared.Contracts │ │ Shared.Kernel │
60 │ IRequest/INotification│ │ BaseEntity, LangStr │
61 └──────────────────────┘ └──────────────────────┘
62 ```
63
64 **Põhimõte:** üks deployable, kolm isoleeritud moodulit. Iga moodul = mini-Clean-Architecture (Domain/Application/Infrastructure/Api). Cross-module kõned eranditult MediatR-iga.
65
66 ### Kursuse loengu võtmelaused (modular monolith — `modularmonolith.md`)
67
68 > *"Modules never reference each other's internals. Module A doesn't touch Module B's entities, repositories, or DbContext."*
69
70 > *"Communication goes through: contracts (interfaces in shared project) and domain events (in-process, loose coupling)."*
71
72 > *"Each module has its own DbContext scoped to its tables — modules don't share database contexts."*
73
74 Meie projekt järgib seda:
75 - `Modules/Users/Application` ei viita `Modules/Trips/*`-le ega `Modules/Expenses/*`-le
76 - Kui `Trips` vajab kasutaja-nime, saadab ta `GetUserByIdQuery` MediatR-i kaudu — Users module's handler vastab
77 - Iga moodul omab oma `DbContext`-i ja Postgres schema (cross-module SQL JOIN-id keelatud)
78
79 ---
80
81 ## 2. Lahenduse struktuur
82
83 ```
84 SplitApp.Modular/
85 ├── SplitApp.sln
86 ├── Directory.Build.props
87 └── src/
88 ├── SplitApp.WebApp/ ← composition root, host
89 │ ├── Program.cs ← DI wiring, AddXxxModule(...)
90 │ ├── Application/ ← Phase 2 BLL liigutatud
91 │ │ ├── Services/ (+ Admin/, Identity/) ← TripService, ExpenseService, ...
92 │ │ ├── DTO/ ← TripBllDto, ExpenseBllDto, ...
93 │ │ ├── Mappers/ ← Domain↔BllDto factory mapperid
94 │ │ ├── Persistence/AppUnitOfWork.cs ← agregeerib 3 mooduli DbContext-id
95 │ │ ├── Persistence/CrossModuleNavigationLoader.cs ← hüdreerib [NotMapped] cross-navsid
96 │ │ └── Contracts/IAppUnitOfWork.cs ← Phase 2 stiilis facade
97 │ ├── Areas/Admin/ ← admin UX
98 │ ├── Areas/Identity/ ← Razor Register
99 │ ├── Controllers/ ← klient MVC
100 │ ├── Views/ ← klient vaated
101 │ └── Resources/ ← i18n .resx
102 ├── Shared/
103 │ ├── SplitApp.Shared.Kernel/ ← BaseEntity, IBaseRepo, IUoW, LangStr, IdentityHelpers
104 │ └── SplitApp.Shared.Contracts/ ← MediatR contracts
105 └── Modules/
106 ├── Users/ ← AppUser, AppRole, AppRefreshToken; JWT issuance
107 ├── Trips/ ← Trip, TripParticipant, TripPoll, TripWishlistItem, BudgetCategory, ...
108 └── Expenses/ ← Expense, ExpenseSplit, SettlementPlan, SettlementPayment, Currency, SplitPreset, ...
109 ```
110
111 `tests/` sisaldab:
112 - 3 mooduli unit-teste (CurrencyConverter, LangStr, IdentityHelpers)
113 - `WebApp.IntegrationTests/Architecture/` — `ModuleBoundaryTests`, `DbContextSchemaIsolationTests`, `CrossModuleNavigationTests`
114 - `WebApp.IntegrationTests/HostBootSmokeTests` + `HostFeatureTests` — `WebApplicationFactory<Program>` HTTP-smoke + ristlõikeliste nõuete testid
115
116 Kokku **44 testi** — kõik green.
117
118 ---
119
120 ## 3. Mooduli piirid — viidete reeglid
121
122 Compiler-enforced + verifitseeritud architecture-testidega.
123
124 | Allikas | Lubatud sihtmärgid | Märkus |
125 |---------|-------------------|--------|
126 | `Modules/X/Application` | sama mooduli `Domain` + `Shared.Kernel` + `Shared.Contracts` | |
127 | `Modules/X/Infrastructure` | sama mooduli `Domain` + `Application` + `Shared.Kernel` | |
128 | `Modules/X/Api` | sama mooduli `Application` + `Shared.Kernel` + `Shared.Contracts` | |
129 | `Shared.*` | mitte ühelegi moodulile | |
130 | `WebApp` | kõik 3 mooduli `Api` + `Infrastructure` + `Shared.*` | composition root |
131
132 **Kõrvalekalle Domain tasandil:** entiteedi-klassidel on cross-module nav-property'd (`Trip.CreatedBy`, `Expense.PaidByUser`, `Trip.DefaultCurrency` jms), kõik `[NotMapped]`-iga märgitud. Et tüübid (`AppUser`, `Currency`, `Expense`) kompileeruksid, on Domain-csproj-idel viited:
133
134 ```
135 Modules/Trips/Domain.csproj → Modules/Users/Domain + Modules/Expenses/Domain
136 Modules/Expenses/Domain.csproj → Modules/Users/Domain
137 ```
138
139 **Miks see olemas on?** Iga moodul käib ainult oma DbContext-i kaudu — ehk `ExpensesDbContext` ei tea midagi `users` skeemist. Aga UI peab näitama "kulu 80€ — maksis Alice Johnson". WebApp-i fassaad ([`AppUnitOfWork`](SplitApp.Modular/src/SplitApp.WebApp/Application/Persistence/AppUnitOfWork.cs)) lahendab selle nii: tõmbab kõigepealt `Expense`-id Expenses-DB-st, siis päring Users-DB-st õigete `AppUser`-ite järgi, ja **käsitsi** C#-koodis paneb `expense.PaidByUser = user`. Et see omistamine oleks **tüübikindel** (kompilaator kontrollib, IDE pakub autocomplete'i, vead leitakse build-i ajal — mitte runtime'is), peab `Expense`-klass teadma `AppUser` tüüpi → siit Domain-csproj-viide.
140
141 `[NotMapped]` tagab samal ajal, et **andmebaas jääb sellest täiesti puutumata** — EF ei tee veergu, ei tee JOIN-i, ei näe seda välja. Schema-isolatsioon säilib täielikult.
142
143 **Kompromiss:** Application/Infrastructure/Api jäävad puhtaks ja kasutavad MediatR-i. Schema-isolatsioon + suhtluse-isolatsioon runtime-tasemel on 100% säilitatud. Ainult entiteedi-*tüübid* on jagatud — see on **C# tüübisüsteemi mugavus mälus-ühendamise jaoks**, mitte funktsionaalne sõltuvus.
144
145 `CrossModuleNavigationTests` kindlustab: kui keegi proovib teha *mapped* (mitte-`[NotMapped]`) cross-module nav-i, siis test kukub.
146
147 ---
148
149 ## 4. Moodulite-vaheline suhtlus (MediatR)
150
151 Lepingud elavad `Shared.Contracts/<Moodul>/{Queries|Events|Commands}/`. Iga leping on `record` mis implementeerib kas:
152 - `IRequest<T>` — sünkroonne päring/käsk (üks vastus)
153 - `INotification` — fan-out sündmus (mitu tellijat)
154
155 Saadetakse hetkel 11 lepingut:
156
157 | Leping | Omanik | Otstarve |
158 |--------|--------|----------|
159 | `GetUserByIdQuery → UserDto?` | Users | Trips/Expenses kasutavad nime kuvamiseks |
160 | `GetUsersByIdsQuery → IReadOnlyList<UserDto>` | Users | Partii-päring |
161 | `UserDeletedEvent` (notification) | Users | Trips + Expenses tellivad — eemaldavad seotud kirjed |
162 | `GetTripByIdQuery → TripSummaryDto?` | Trips | Cross-module reisi-otsing |
163 | `GetTripParticipantsQuery → IReadOnlyList<TripParticipantDto>` | Trips | |
164 | `IsTripParticipantQuery → bool` | Trips | **IDOR-i kaitse**: ExpensesController kasutab seda enne expense-i salvestamist |
165 | `TripDeletedEvent` (notification) | Trips | Expenses tellib — kustutab kulud + arveldused |
166 | `GetTripExpenseTotalsQuery → TripExpenseTotalsDto` | Expenses | |
167 | `GetBudgetCategorySpentQuery → IReadOnlyDictionary<Guid, decimal>` | Expenses | Eelarve-kategooria kulutused |
168 | `ExpenseSettledEvent` (notification) | Expenses | Reserveeritud tulevikuks |
169 | `SettlementPlanCompletedEvent` (notification) | Expenses | Trips tellib — kui kõik maksed kinnitatud, märgib reisi "Settled" |
170
171 **Näide voost:** kasutaja loob expense-i (`POST /api/v1/expenses`):
172 1. `ExpensesController` saadab `IsTripParticipantQuery(tripId, userId)` → MediatR
173 2. Trips mooduli `IsTripParticipantHandler` kontrollib `TripsDbContext`-st — tagastab `bool`
174 3. Kui `false` → 403 Forbidden (IDOR-i kaitse)
175 4. Kui `true` → expense salvestub `ExpensesDbContext`-i (schema `expenses`)
176
177 ---
178
179 ## 5. Andmeisolatsioon
180
181 Üks Postgres andmebaas, kolm schemat. Kolm `DbContext`-i ühenduvad sama `ConnectionStrings:DefaultConnection`-i kaudu, aga igaüks kasutab `b.HasDefaultSchema("...")`-d:
182
183 | DbContext | Schema | Sisu |
184 |-----------|--------|------|
185 | `UsersDbContext : IdentityDbContext<AppUser, AppRole, Guid>` | `users` | AspNetUsers, AspNetRoles, RefreshTokens, DataProtectionKeys |
186 | `TripsDbContext : DbContext` | `trips` | Trips, TripParticipants, TripInvitations, TripPolls, TripWishlistItems, BudgetCategories |
187 | `ExpensesDbContext : DbContext` | `expenses` | Expenses, ExpenseSplits, SettlementPlans, SettlementPayments, Currencies, SplitPresets |
188
189 **Cross-module SQL JOIN-id on keelatud.** EF kunagi ei lähe schema piirist üle, sest cross-module nav-property'd on `[NotMapped]`. Cross-module andmete komponeerimine toimub:
190 1. WebApp facade `AppUnitOfWork`-is — repod hüdreerivad cross-module navsid C#-is pärast põhipäringut (vt `CrossModuleHydration.HydrateUsersAsync`, `HydrateTripsAsync`)
191 2. Või MediatR-i kaudu — `ExpensesController` küsib Users-mooduli käest `GetUsersByIdsQuery`-iga
192
193 **Andmete terviklikkus** (cross-module FK puudub) tagatakse:
194 - Eel-MediatR valideerimispäringutega (`IsTripParticipantQuery`)
195 - Domeeni-sündmustega kustutamisel (`UserDeletedEvent`, `TripDeletedEvent`)
196
197 ---
198
199 ## 6. Phase 2 BLL "lifted" struktuur WebApp-is
200
201 Phase 3 ei loo uut BLL-i; selle asemel **liigutab Phase 2 BLL koodi `WebApp/Application/`** alla. See on praktiline kompromiss, mis hoiab Phase 2 100% paarsust UX-iga ja säästab refaktori-aega.
202
203 | Phase 2 projekt | Phase 3 sihtmärk |
204 |---|---|
205 | `App.BLL/Services/*` (Trip, Expense, Settlement, ...) | `WebApp/Application/Services/*` |
206 | `App.BLL/Services/Admin/*` (12 admin-teenust) | `WebApp/Application/Services/Admin/*` |
207 | `App.BLL/Services/Identity/*` | `Modules/Users/Application/Services/` (siiski liigutatud Users-moodulisse) |
208 | `App.BLL/DTO/*` | `WebApp/Application/DTO/*` |
209 | `App.BLL/Mappers/*BllDtoFactory.cs` | `WebApp/Application/Mappers/*` |
210 | `App.Domain/Contracts/IAppUnitOfWork.cs` | `WebApp/Application/Contracts/IAppUnitOfWork.cs` |
211 | `App.DAL.EF.AppUnitOfWork` | `WebApp/Application/Persistence/AppUnitOfWork.cs` (3 DbContext-i agregaator) |
212 | `App.DAL.EF.Repositories.*` | inline `WebApp/Application/Persistence/AppUnitOfWork.cs` (TripRepo, ExpenseRepo jne) |
213
214 Lifted BLL teenused töötavad endiselt `IAppUnitOfWork`-i kaudu. Teenuse vaatest pole midagi muutunud — `_uow.Trips.GetByIdAsync(...)` käitub samamoodi nagu Phase 2-s. Tegelikkuses suunab `AppUnitOfWork.Trips` päringud `TripsDbContext`-le, ja `CrossModuleHydration` täidab cross-module nav-id (`Trip.CreatedBy` jms) tagantjärele eraldi päringuga.
215
216 ---
217
218 ## 7. Käivitamine
219
220 **Tootmine (deployd):** https://travel.rasmusj.com/
221
222 **Lokaalselt** repo juurest:
223
224 ```bash
225 docker compose up --build
226 ```
227
228 Tõuseb üles:
229 - `phase3` → http://localhost:90 (host port `90` → container port `8080`)
230 - `phase3-db` (PostgreSQL 16) — ainult Docker sisevõrgus, host port pole avatud
231
232 Iga mooduli migratsioonid jooksevad automaatselt host-i käivitumisel (vt `*ModuleExtensions.UseXxxModule(...)`-it).
233
234 ### Testid
235
236 ```bash
237 cd SplitApp.Modular
238 dotnet test
239 ```
240
241 Roheliseks läheb **44 testi**:
242
243 | Projekt | Testid | Mida katavad |
244 |---|---:|---|
245 | `SplitApp.Modules.Users.Tests` | 8 | `IdentityHelpers` — JWT genereerimine + valideerimine + tagasilükkamine vale issuer/audience/võtme/malformed-tokeni puhul |
246 | `SplitApp.Modules.Trips.Tests` | 12 | `LangStr` — mitme-keele tõlke teisendus + fallback + tühi/null/error piirjuhud |
247 | `SplitApp.Modules.Expenses.Tests` | 10 | `CurrencyConverter` — kursi-teisendused + null/negatiivsed summad + ümardamine + round-trip täpsus |
248 | `SplitApp.WebApp.IntegrationTests` | 14 | Arhitektuuri invariandid (mooduli piirid, schema-isolatsioon, `[NotMapped]` reegel) + `WebApplicationFactory` HTTP-smoke (Home/, Swagger UI + v1 doc, Admin auth-redirect, REST API JWT-nõue, lokaliseerimine `?culture=et`) |
249
250 **Unit-testid** ei vaja andmebaasi — käivituvad millisekundites ja testivad puhast loogikat (kursi-teisendus, JWT-token, LangStr). **Integration-testid** käivitavad reaalse WebApp hosti mälus (`WebApplicationFactory<Program>` + `"Testing"` keskkond, kus migratsioonid skiipitakse) ja teevad HTTP-päringuid — see kinnitab, et iga ristlõikeline nõue (Swagger, JWT, Admin kaitse, i18n) on päriselt töökorras, mitte ainult konfiguratsioonis olemas.
251
252 ---
253
254 ## 8. URL kaart
255
256 | URL | Otstarve |
257 |-----|----------|
258 | `/` | Avalehe MVC |
259 | `/Trips`, `/Trips/Create`, `/Trips/Details/{id}`, ... | Reisi CRUD |
260 | `/Members?tripId={id}` ja `/Members/AcceptInvitation/{token}` | Osalejad + kutse |
261 | `/Expenses?tripId={id}` (Create/Edit/Delete) | Kulud reisi kohta |
262 | `/Budget?tripId={id}` (CreateCategory/EditCategory/DeleteCategory) | Eelarvekategooriad |
263 | `/Settlement?tripId={id}` | Bilanss + arveldusplaanid |
264 | `/PollsClient?tripId={id}` (Create/Details) | Reisi küsitlused |
265 | `/WishlistClient?tripId={id}` | Soovinimekiri |
266 | `/Identity/Account/Register` | Cookie-põhine registreerumine |
267 | `/Admin/Dashboard` | Admin avaleht (`admin` roll) |
268 | `/Admin/{Users, Trips, Expenses, BudgetCategories, Currencies, Invitations, Polls, SettlementPlans, SettlementPayments, SplitPresets, TripParticipants, Wishlist}` | Admin CRUD |
269 | `/swagger` | Swagger UI |
270
271 ### REST API
272
273 | Moodul | Endpoint-id |
274 |--------|-------------|
275 | Users | `/api/v1/identity/account/{register, login, logout, refreshtokendata}` |
276 | Trips | `/api/v1/trips`, `/api/v1/budgetcategories`, `/api/v1/invitations`, `/api/v1/polls`, `/api/v1/wishlist` |
277 | Expenses | `/api/v1/expenses`, `/api/v1/currencies`, `/api/v1/settlements`, `/api/v1/splitpresets` |
278
279 ---
280
281 ## 9. Architecture-testid (mooduli piiride lukustamine)
282
283 `tests/SplitApp.WebApp.IntegrationTests/Architecture/`:
284
285 1. **`ModuleBoundaryTests`** — ükski mooduli `Application`/`Infrastructure`/`Api` `<ProjectReference>` ei viita teisele moodulile; `Shared.*` ei viita ühelegi moodulile.
286 2. **`DbContextSchemaIsolationTests`** — iga `DbContext` sisaldab `DbSet<T>`-e ainult oma mooduli `Domain` projektist.
287 3. **`CrossModuleNavigationTests`** — cross-module nav-property on lubatud ainult `[NotMapped]`-iga.
288 4. **`HostBootSmokeTests`** — `WebApplicationFactory<Program>` käivitab täis-host'i `Testing` keskkonnas (skiipib migratsioonid), `/`, `/Home/Index` ja autentimata API-päring tagastab 401.
289 5. **`HostFeatureTests`** — ristlõikeliste nõuete HTTP-tasandil kontroll: Swagger v1 doc + UI serveeritakse, `/Admin/*` suunab autentimata kasutaja Identity login-lehele, `/api/v1/expenses/*` nõuab JWT-d (teine moodul, sama reegel), `?culture=et` ei riku request-localization ahelat.
290
291 Kui mõni neist langeb, on keegi rikkunud modulaarmonoliidi invariandi või ristlõikelise nõude.
292
293 ---
294
295 ## 10. Miks modulaarne monoliit?
296
297 | Lähenemine | Probleem |
298 |------------|----------|
299 | Klassikaline monoliit | Kõik viitab kõigele — üks muudatus → kaskaad-mõju |
300 | Mikroteenused | Hajusüsteemide põrgu — võrk, serialiseerimine, eventual consistency, deployment-keerukus |
301 | **Modulaarne monoliit** | **Selged piirid (nagu mikroteenustel) + lihtne deployment (nagu monoliidil)** |
302
303 Ekstraheerimise tee tulevikus, kui peaks vaja minema:
304 - *Klassikaline monoliit → Modulaarne monoliit → Mikroteenused*
305 - Iga moodul juba omab oma schema, oma lepingud, oma `DbContext`-i. Mooduli eraldamine teenuseks tähendab in-process MediatR-kõnete asendamist HTTP/gRPC-ga ning sündmuste viimist message brokeri peale. Koodi struktuur ei muutu — ainult transport-kiht.
306
307 Vaata kursuse [modularmonolith.md](modularmonolith.md) faili pikemaks aruteluks.
308
309 ---
310
311 ## Kokkuvõte
312
313 **SplitApp Phase 3 on modulaarne monoliit kolme isoleeritud mooduliga (Users, Trips, Expenses).** Iga moodul on iseseisev mini-Clean-Architecture oma `Domain`/`Application`/`Infrastructure`/`Api` projektidega ja oma Postgres schema. Cross-module suhtlus käib ainult **MediatR**-i kaudu (`IRequest`/`INotification`), mitte otseste `<ProjectReference>`-ite kaudu. Architecture-testid lukustavad need invariandid CI ajal. Kõigil Phase 2 nõuetel (REST API + versioning + Swagger, JWT, MVC + Admin Area, i18n, IDOR, Repos/UoW/Services/BLL/Mappers, CI/CD, testid) on Phase 3-s täielik kate.
314