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 | |