SplitApp — Modulaarne Monoliit (Phase 3)
1. Üldine pilt
┌────────────────────────────────────────────────────┐
│ SplitApp.WebApp (Composition Root) │
│ Program.cs · MVC Controllers · Areas/Admin │
│ Areas/Identity · Views · ViewModels │
│ Application/{Services, DTO, Mappers, Persistence}│
│ (lifted phase-2 BLL — kasutab IAppUnitOfWork) │
└────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Users │ │ Trips │ │ Expenses │
│ │ │ │ │ │
│ Domain │ │ Domain │ │ Domain │
│ Application│ │ Application│ │ Application│
│ Infra │ │ Infra │ │ Infra │
│ Api │ │ Api │ │ Api │
│ │ │ │ │ │
│ schema: │ │ schema: │ │ schema: │
│ users │ │ trips │ │ expenses │
└─────────────┘ └─────────────┘ └─────────────┘
│ ▲ │ ▲ │ ▲
│ │ MediatR │ │ MediatR │ │ MediatR
└──┴─────────────┴──┴─────────────┴──┘
┌─────────────────────────┐
│ Shared.Contracts │
│ IRequest / INotification│
│ (Queries + Events) │
└─────────────────────────┘
┌─────────────────────────┐
│ Shared.Kernel │
│ BaseEntity · LangStr │
│ IdentityHelpers (JWT) │
└─────────────────────────┘
Põhimõte: üks deployment, kolm sisemiselt isoleeritud moodulit. Iga moodul omab oma domeeni, andmeid (Postgres schema) ja teenuseid. Moodulid suhtlevad ainult MediatR-i kaudu — mitte ühtegi otseviidet teise mooduli sisemusele.
2. Lahenduse struktuur (SplitApp.Modular/)
SplitApp.sln
├── src/
│ ├── SplitApp.WebApp/ ← composition root, host, MVC + admin
│ │ ├── Program.cs ← DI wiring, AddXxxModule(...)
│ │ ├── Application/ ← Phase 2 BLL liigutatud siia
│ │ │ ├── Services/ (+ Admin/, Identity/) ← TripService, ExpenseService, ...
│ │ │ ├── DTO/ ← TripBllDto, ExpenseBllDto, ...
│ │ │ ├── Mappers/ ← Domain↔BllDto factory mapperid
│ │ │ └── Persistence/ ← AppUnitOfWork (3 mooduli DbContexti agregaator)
│ │ ├── Areas/Admin/ ← admin UX, ViewModels, eraldi layout
│ │ ├── Areas/Identity/ ← Razor Pages (Register)
│ │ ├── Controllers/ ← klient-MVC kontrollerid
│ │ └── Views/ ← klient-vaated
│ ├── Shared/
│ │ ├── SplitApp.Shared.Kernel/ ← BaseEntity, IBaseRepository, IUnitOfWork, LangStr, IdentityHelpers
│ │ └── SplitApp.Shared.Contracts/ ← MediatR IRequest / INotification
│ └── Modules/
│ ├── Users/
│ │ ├── SplitApp.Modules.Users.Domain/ ← AppUser, AppRole, AppRefreshToken
│ │ ├── SplitApp.Modules.Users.Application/ ← IIdentityService, MediatR handlerid
│ │ ├── SplitApp.Modules.Users.Infrastructure/ ← UsersDbContext, repod, AddUsersModule
│ │ └── SplitApp.Modules.Users.Api/ ← /api/v1/identity/...
│ ├── Trips/ ← sama 4-projekti struktuur, schema "trips"
│ └── Expenses/ ← sama 4-projekti struktuur, schema "expenses"
└── tests/
├── SplitApp.Modules.Users.Tests/
├── SplitApp.Modules.Trips.Tests/
├── SplitApp.Modules.Expenses.Tests/
└── SplitApp.WebApp.IntegrationTests/ ← architecture invariants + smoke
3. Viidete reeglid (compiler-enforced + arch-test verified)
| Allikas | Lubatud sihtmärgid | Märkused |
|---|---|---|
Modules/X/<Layer> (Application/Infrastructure/Api) |
sama mooduli teised projektid + Shared.Kernel + Shared.Contracts |
Mitte teise mooduli projektidele |
Shared.* |
mitte ühelegi moodulile | |
WebApp |
kõik kolm moodulit (Api + Infrastructure) + Shared | composition root |
Erand Domain tasandil: Phase 2 vaate-renderdamise paarsuse hoidmiseks (Trip.CreatedBy.Email, Expense.PaidByUser.FirstName jne) on entiteedid säilitanud cross-module nav-property'd, kuid annotatsiooniga [NotMapped], et EF kunagi ei ületaks Postgres schema piiri. Vaata SplitApp.Modular/docs/ARCHITECTURE.md [NotMapped] lõiku.
Architecture-testid jälgivad neid invariante (tests/SplitApp.WebApp.IntegrationTests/Architecture/):
ModuleBoundaryTests— ükski mooduliApplication/Infrastructure/Apiei viita teisele moodulileDbContextSchemaIsolationTests— iga DbContext sisaldab DbSet-e ainult oma mooduli entiteetideleCrossModuleNavigationTests— cross-module nav-property on lubatud ainult[NotMapped]-igaHostBootSmokeTests—WebApplicationFactory<Program>käivitab täis-host'i
4. Moodulite-vahene suhtlus (MediatR)
Kõik kõned üle mooduli piiri lähevad läbi MediatR. Lepingud (IRequest<T> või INotification) elavad Shared.Contracts/<Moodul>/{Queries|Events|Commands}/. Handlerid omanik-mooduli Application või Infrastructure kihis.
Saadetakse hetkel:
| Leping | Omanik | Otstarve |
|---|---|---|
GetUserByIdQuery : IRequest<UserDto?> |
Users | Trips/Expenses kasutavad nime kuvamiseks |
GetUsersByIdsQuery : IRequest<IReadOnlyList<UserDto>> |
Users | Partii-päring |
UserDeletedEvent : INotification |
Users | Trips + Expenses tellivad — eemaldavad seotud kirjed |
GetTripByIdQuery : IRequest<TripSummaryDto?> |
Trips | Cross-module reisi-otsing |
GetTripParticipantsQuery : IRequest<IReadOnlyList<TripParticipantDto>> |
Trips | |
IsTripParticipantQuery : IRequest<bool> |
Trips | IDOR-i kaitse Expenses controller-is |
TripDeletedEvent : INotification |
Trips | Expenses tellib — kustutab kulud + arveldused |
GetTripExpenseTotalsQuery : IRequest<TripExpenseTotalsDto> |
Expenses | |
GetBudgetCategorySpentQuery : IRequest<...> |
Expenses | Eelarvekategooria kulutused |
SettlementPlanCompletedEvent : INotification |
Expenses | Trips tellib — märgib reisi "Settled" kui kõik makstud |
5. Andmeisolatsioon
Iga moodul omab oma DbContext-i ja Postgres schema:
UsersDbContext : IdentityDbContext<AppUser, AppRole, Guid>→ schemausersTripsDbContext : DbContext→ schematripsExpensesDbContext : DbContext→ schemaexpenses
Kõik kolm ühenduvad samasse Postgres andmebaasi (sama ConnectionStrings:DefaultConnection). Schemad — mitte eraldi DB-d — annavad isolatsiooni. Cross-module SQL JOIN-id on keelatud; cross-module andmed komponeerib WebApp facade läbi MediatR ja CrossModuleNavigationLoader-i.
Cross-module entiteedi-viited on lihtsad Guid väljad ilma EF foreign-key piiranguta (nt Trip.CreatedById : Guid viitab kontseptuaalselt users.AspNetUsers.Id-le, aga mitte FOREIGN KEY kaudu). Andmete terviklikkus tagatakse:
- Eel-MediatR valideerimispäringutega (nt
IsTripParticipantQueryenne expense split-i salvestamist) - Domeeni-sündmustega kustutamisel (
UserDeletedEvent,TripDeletedEvent)
6. Sõltuvuste graaf (mooduli sees — Clean Architecture)
Iga moodul on iseseisev mini-Clean-Architecture:
Shared.Kernel (BaseEntity, contracts)
▲
│
Module.Domain ◄─── (entiteedid, enumid)
▲
┌──────┴──────┐
│ │
Module.Application │
▲ │
│ │
Module.Infrastructure (DbContext, repod, EF migrations)
▲
│
Module.Api (REST controllers, DTO-d)
▲
│
WebApp (composition root)
Iga mooduli Infrastructure registreerib enda AddXxxModule(IConfiguration) extension-meetodi. WebApp/Program.cs kutsub kõik kolm:
builder.Services.AddUsersModule(builder.Configuration);
builder.Services.AddTripsModule(builder.Configuration);
builder.Services.AddExpensesModule(builder.Configuration);
7. Phase 3 ↔ Phase 2 vastendus
| Phase 2 projekt (kustutatud) | Phase 3 sihtmärk |
|---|---|
Base.Domain, Base.Contracts |
Shared.Kernel |
Base.Helpers (IdentityHelpers) |
Shared.Kernel.Auth |
App.Domain.Identity.* |
Modules/Users/SplitApp.Modules.Users.Domain/Entities/ |
App.Domain.{Trip, TripParticipant, ...} |
Modules/Trips/SplitApp.Modules.Trips.Domain/Entities/ |
App.Domain.{Expense, SettlementPlan, ..., Currency} |
Modules/Expenses/SplitApp.Modules.Expenses.Domain/Entities/ |
App.DAL.EF.AppDbContext |
jagatud 3-ks: UsersDbContext, TripsDbContext, ExpensesDbContext |
App.BLL.Services.Identity.* |
Modules/Users/.../Application/Services/ |
App.BLL.Services.* (Trip, Expense, Settlement, ...) |
WebApp/Application/Services/ (composition-root facade kõigi 3 mooduli UoW peal) |
App.BLL.{DTO, Mappers} |
WebApp/Application/{DTO, Mappers} |
App.Resources/Domain/*.resx |
(üks ühtne) WebApp/Resources/Views/Shared.{resx,et.resx} |
WebApp.ApiControllers.Identity.* |
Modules/Users/.../Api/Controllers/ |
WebApp.ApiControllers.{Trips, ...} |
Modules/Trips/.../Api/Controllers/ |
WebApp.ApiControllers.{Expenses, Currencies, Settlements, SplitPresets} |
Modules/Expenses/.../Api/Controllers/ |
WebApp/{Controllers, Areas/Admin, Areas/Identity, Views} |
SplitApp.WebApp/{Controllers, Areas/Admin, Areas/Identity, Views} (struktuur sama; namespace re-rooted SplitApp.WebApp.*) |
8. Käivitamine
Tootmine (deployd): https://travel.rasmusj.com/
Lokaalselt:
# Repo juurest
docker compose up --build
Tõuseb üles:
phase3→ http://localhost:90 (host port90→ container port8080)phase3-db(PostgreSQL 16) — ainult Docker sisevõrgus, host port pole avatud
Iga mooduli migratsioonid jooksevad automaatselt host-i käivitamisel (vt *ModuleExtensions.UseXxxModule()).
# Testid (25 testi 4 projektis)
cd SplitApp.Modular
dotnet test
9. Miks modulaarne monoliit
| Lähenemine | Probleem |
|---|---|
| Klassikaline monoliit | "Kõik viitab kõigele" — üks muudatus → kaskaad-mõju mujal |
| Mikroteenused | Hajusüsteemide põrgu — võrk, serialiseerimine, eventual consistency |
| Modulaarne monoliit | Selged piirid (nagu mikroteenustel) + lihtne deployment (nagu monoliidil) |
Vaata pikemat juttu kursuse modularmonolith.md failist või SplitApp.Modular/docs/ARCHITECTURE.md-ist.
Kokkuvõte
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 eranditult MediatR-i kaudu (IRequest/INotification), mitte otsesete <ProjectReference>-ite kaudu. Architecture-testid lukustavad need invariandid CI-ajal.