SplitApp — Reisikulude haldamise rakendus
Ülevaade
SplitApp on ASP.NET Core 10.0 veebirakendus grupireisi kulude jagamiseks ja haldamiseks. Rakendus võimaldab kasutajatel luua reise, kutsuda sõpru, lisada kulusid paindliku jagamisega, hallata eelarvet, teha küsitlusi, pidada soovinimekirja ja arveldada võlgu optimeeritud algoritmiga. Rakendus kasutab Clean Architecture't: sõltuvused liiguvad sissepoole Domain-i, interfejsid elavad Domain-kihis (App.Domain/Contracts/) ja App.DAL.EF on "plugin", mis neid implementeerib. App.BLL sõltub ainult Domain-abstraktsioonidest ja WebApp kontrollerid kasutavad ainult BLL teenuseid — mitte kunagi IAppUnitOfWork-i ega repositore otse.
Projekt on tehtud TalTech kursuse "Web Applications with C#" Personal Project — Phase 1 raames.
0. Nõuete täitmine (Assignment requirements)
Phase 1 ülesande järgi peavad olemas olema järgmised asjad. Alljärgnevas tabelis on iga nõue, selle täitmise staatus ja konkreetne asukoht koodis.
| # | Nõue | Staatus | Kus näha |
|---|---|---|---|
| 1 | Domeenikujundus: min 10 mõtestatud entiteeti | ✅ 16 entiteeti | App.Domain/ — Trip, Expense, ExpenseSplit, BudgetCategory, Currency, TripParticipant, TripPoll, TripPollOption, TripPollVote, TripWishlistItem, TripWishlistVote, SplitPreset, SplitPresetMember, TripInvitation, SettlementPlan, SettlementPayment + 3 Identity entiteeti |
| 2 | REST API + versioneerimine + avalikud DTO-d | ✅ | WebApp/ApiControllers/, [ApiVersion("1.0")], marsruut /api/v{version:apiVersion}/[controller], DTO-d App.DTO/v1/ |
| 3 | Swagger | ✅ | /swagger endpoint, ConfigureSwaggerOptions.cs — Bearer auth + versioneerimine integreeritud |
| 4 | Autentimine (JWT) | ✅ | Program.cs JWT Bearer konfiguratsioon; AccountController.cs — register, login, refreshtoken, logout |
| 5 | Kliendi UX (MVC, scaffolded) | ✅ | WebApp/Controllers/ — 8 MVC kontrollerit; standardsed CRUD-vaated, tõestavad domeeni toimimise |
| 6 | Admin UX (MVC, Area, kaitstud, kujundatud, ViewModelid, no ViewBag/ViewData) | ✅ | WebApp/Areas/Admin/ — 13 kontrollerit, [Authorize(Roles = "admin")], oma sidebar-layout, admin.css, custom Dashboard. 0 ViewData/ViewBag kasutust — grep kontrollitud |
| 7 | UI tõlked (i18n, .resx) | ✅ EN + ET | App.Resources/ — Shared.resx, Common.resx, Domain/* (Trip, Expense, Currency, BudgetCategory jne) |
| 8 | Andmebaasi tõlked (LangStr) | ✅ | Base.Domain/LangStr.cs; kasutatud Currency.Name ja BudgetCategory.Name väljadel — JSON-ina PostgreSQL-is |
| 9 | IDOR kaitse (kasutaja näeb ainult oma andmeid) | ✅ | Kaitse elab BLL teenustes: iga meetod, mis puudutab reisi-andmeid, võtab Guid userId ja kontrollib osaleja/organiseerija staatust sees. WebApp kontrollerid ei pääse UoW-le üldse — kontrolli vahele jätta on võimatu |
| 10 | CI/CD deploy (äpp + DB) | ✅ | .gitlab-ci.yml — docker compose up --build main harul; Dockerfile multi-stage; docker-compose.yml — app + PostgreSQL 16 + persistent volume + automaatne migreerimine ja seeding |
| 11 | Admin pole lihtsalt scaffold — "designed, nice, good to use" | ✅ | Eraldi admin layout (sidebar + topbar), admin.css — metric cards, status badges, timeline feed, empty states; Dashboard custom statistikaga (Top Active Trips, Biggest Expenses, User Activity 7d/30d, Top Active Users, Activity Feed) |
Lisaks (pole nõutud, aga olemas):
- Repository pattern (Base.Contracts/IBaseRepository.cs + entiteedispetsiifilised repositoryd)
- Unit of Work pattern (
IAppUnitOfWork) - Service layer äriloogika jaoks (
SettlementServicegreedy algoritm,ExpenseService4 split-meetodit,InvitationServicetoken-põhised kutsed,PollServicehääletamise toggle) - Manuaalsed DTO mapperid (ei kasuta AutoMapper-it)
1. Arhitektuur — Clean Architecture
Projekt kasutab Clean Architecture't. Sõltuvused liiguvad sissepoole (Dependency Inversion Principle): kõik kihid sõltuvad Domain-ist (või millestki sisemisest), mitte väliskihtidest.
Kursuse loengu (architecture1) võtmelause
"The entire difference between N-tier and Clean Architecture is who owns the interfaces." — "Move IPersonRepository from DAL into Domain, and your dependency arrow flips."
Meie projekt järgib seda põhimõtet:
IAppUnitOfWorkja 10 repository-interfejsi elavad App.Domain/Contracts/-is, mitte DAL-is.App.DAL.EF(infrastructure) implementeerib neid interfejse — on "plugin" Domain-kihi peal.App.BLL.csprojei viita enamApp.DAL.EF-ile — ainultApp.Domain-ile jaApp.DTO-le.- WebApp kontrollerid (kõik 23: client MVC + API + Admin) ei kasuta
IAppUnitOfWork-i ega repositore otse — ainult BLL teenuseid.
Sõltuvuse graaf
Base.Contracts (IBaseEntity, IBaseRepository, IUnitOfWork)
▲
│
Base.Domain (BaseEntity, LangStr)
▲
│
App.Domain ← SEES
+ Contracts/ (IAppUnitOfWork, 10 × I*Repository)
▲
┌──────┴──────┐
│ │
App.DAL.EF App.DTO
(implem.) │
│
App.BLL (Services — sõltub AINULT Domain+DTO)
▲
│
WebApp (Controllers — kasutavad BLL teenuseid)
(DAL viide ainult Program.cs DI jaoks
→ AddDalServices() extension method)
Clean-i võtmeomadused (verifitseeritavad):
grep "using App.DAL.EF" App.BLL/→ 0 tulemustgrep "IAppUnitOfWork\|_uow\." WebApp/Controllers/ WebApp/ApiControllers/ WebApp/Areas/→ 0 tulemustApp.BLL.csprojrefs: ainultApp.Domain,App.DTOApp.DAL.EFviitab Domain-i interfejsidele ja implementeerib neid (AppUnitOfWork : IAppUnitOfWorkDomain-ist)
Projekti kihid
Base.Contracts ← Geneerilised liidesed (IBaseEntity, IBaseRepository, IUnitOfWork)
Base.Domain ← Base-entiteedid (BaseEntity, LangStr)
Base.Helpers ← JWT genereerimine/valideerimine
App.Domain ← 16 domeeni entiteeti + 8 enum-i + Contracts/ (IAppUnitOfWork + I*Repository)
App.DAL.EF ← AppDbContext, AppUnitOfWork, repository-implementatsioonid, migratsioonid,
ServiceCollectionExtensions.AddDalServices()
App.DTO ← DTO-d (v1/) + manuaalsed Mapper klassid (ei kasuta AutoMapper-it)
App.BLL ← Application-kiht, teenused koos äriloogikaga
Services/ — Trip, Expense, Settlement, Invitation, Poll,
BudgetCategory, Wishlist, SplitPreset
Services/Admin/ — 12 admin-teenust (üks iga admin-sektsiooni jaoks)
App.Resources ← .resx tõlkefailid (EN + ET)
WebApp ← MVC + API + Admin kontrollerid, vaated, ViewModelid, Program.cs
Miks Clean Architecture?
- Dependency Inversion — WebApp kontrollerid sõltuvad BLL liidestest, BLL sõltub Domain liidestest. Kui tahame DAL-i vahetada (nt MongoDB), asendame ainult
App.DAL.EF— ülejäänud projekt ei muutu. - Testitavus — iga teenust ja repositoryd saab mockida, sest kõik sõltuvused on interface'id ja elavad sees (Domain-is). BLL-i teste saab kirjutada ilma päris andmebaasita.
- Separation of Concerns — äriloogika (settlement algoritm, expense splitting, token-kutsed) elab ainult BLL-is; andmeligipääs ainult DAL-is; HTTP-mure ainult WebApp-is.
- IDOR kaitse tsentraliseeritud — iga BLL teenuse meetod, mis puudutab reisi-andmeid, võtab vastu
Guid userIdparameetri ja kontrollib osaleja/organiseerija staatust teenuse sees. Kontroller ei saa kogemata kontrolli vahele jätta.
App.DAL.EF kui plugin
Kuigi DAL sõltub Domain-ist (järgides Clean reeglit), jääb DAL "väliseks" Domain-i suhtes. Program.cs kutsub builder.Services.AddDalServices(connectionString) — üks composition-root rida — mis registreerib AppDbContext ja IAppUnitOfWork → AppUnitOfWork. WebApp kontrollerid pole teadlikud DAL-i implementatsioonist. See on Clean-i "plugin architecture" omadus.
2. Repository ja Unit of Work muster
Repository muster
Iga entiteet on kättesaadav läbi repository liidese. Geneerilised operatsioonid on defineeritud IBaseRepository<TEntity> liideses (Base.Contracts):
GetAllAsync()— kõik kirjed (Task<IEnumerable<TEntity>>)GetByIdAsync(Guid id)— üks kirje ID järgi (Task<TEntity?>)Add(entity)— lisa uus (sünkroonne — tegelik salvestus toimubSaveChangesAsync()kaudu)Update(entity)— uuenda olemasolevat (sünkroonne)RemoveAsync(Guid id)— kustuta (Task<TEntity?>)ExistsAsync(Guid id)— kontrolli olemasolu (Task<bool>)
Geneerilise baasrepository (BaseRepository<TEntity>) peal on ehitatud entiteedispetsiifilised repositoryd oma päringumeetoditega. Näiteks TripRepository:
GetUserTripsAsync(Guid userId)— kasutaja reisid koos valuuta ja osalejatega (Include)GetByIdWithDetailsAsync(Guid id)— reis kõigi seostegaRemoveAsync(Guid id)— override, mis teostab kaskaadse kustutamise õiges järjekorras (lapselapsed → lapsed → reis), kuna kõik võõrvõtmed onDeleteBehavior.Restrict
TripParticipantRepository on arhitektuuri selgroog — sisaldab IsParticipantAsync() ja IsOrganizerAsync() meetodeid, mida kasutavad KÕIK kontrollerid autoriseerimiseks. Enne refaktoreerimist oli see loogika kopeeritud igasse kontrollerisse eraldi.
Unit of Work muster
IAppUnitOfWork koondab kõik repositoryd üheks liideseks ja haldab SaveChangesAsync() kutsumist:
IAppUnitOfWork
├── Trips (ITripRepository)
├── Expenses (IExpenseRepository)
├── TripParticipants (ITripParticipantRepository)
├── TripInvitations (ITripInvitationRepository)
├── SettlementPlans (ISettlementPlanRepository)
├── TripPolls (ITripPollRepository)
├── TripWishlistItems (ITripWishlistItemRepository)
├── SplitPresets (ISplitPresetRepository)
├── BudgetCategories (IBudgetCategoryRepository)
├── GetRepository<T>() — geneeriliste entiteetide jaoks
└── SaveChangesAsync() — salvestab KÕIK muudatused atomaarselt
Kursuse loeng ütleb: "DbContext already is a Unit of Work." Meie AppUnitOfWork on selle peale ehitatud kiht, mis annab puhta liidese ja peidab EF Core detailid.
Repositoryd on lazy-initsialiseeritud — luuakse ainult siis, kui neid esimest korda kasutatakse.
3. Teenuste kiht (App.BLL)
Teenused sisaldavad äriloogikat, mida kontrollerid ei peaks ise teadma. Teenused sõltuvad IAppUnitOfWork liidesest (mitte DbContext-ist otse).
SettlementService — arvelduse äriloogika
Kõige keerulisem teenus. Põhimeetodid + guarded wrapper'id IDOR kaitseks:
CalculateBalancesAsync(Guid tripId)— arvutab iga osaleja kohta: kui palju on maksnud vs kui palju peab maksma. KasutabCurrencyConverter-it valuuta normaliseerimiseks vaikevaluutasse. Tagastab saldod sorteerituna kahanevas järjekorras.CalculateSettlementAsync(Guid tripId, Guid userId)— greedy algoritm, mis paardab suurima võlausaldaja suurima võlgnikuga, minimeerides maksete arvu. Kasutab kahte sorteeritud nimekirja (võlausaldajad ja võlgnikud) ning two-pointer lähenemist. Lävi: 0.01m (väldib ümardamisartefakte). LoobSettlementPaymentkirjed staatusega Pending.MarkPaidAsync(Guid paymentId, Guid userId)— võlgnik märgib makse tehtuks. Muudab staatust Pending → MarkedPaid, salvestab kuupäeva. AinultFromUserIdsaab seda teha. Guarded variantMarkPaidGuardedAsynckontrollib osalust ja tagastab(ok, errorCode)tupli — kontroller tõlgib"forbidden"→Forbid().ConfirmPaymentAsync(Guid paymentId, Guid userId)— võlausaldaja kinnitab makse laekumist. Muudab staatust MarkedPaid → Confirmed. AinultToUserIdsaab seda teha (guarded variantConfirmPaymentGuardedAsyncjõustab selle). Pärast mutatsiooni laaditakse plaanSettlementPlanRepository.GetByIdAsync-iga (koos Payments-iga), et kontrollida kas kõik maksed on kinnitatud. NB! DAL on konfigureeritudQueryTrackingBehavior.NoTrackingWithIdentityResolution-iga — iga laetud entiteet on detached, mistõttu mutatsioonide salvestamiseks on vajalik selgesõnalinepaymentRepo.Update(payment)kutse (vastasel juhulSaveChangesAsyncei näe muudatust ja andmebaasi ei kirjutata midagi). Plaani ja reisi uuendamine käib shallow base-repo kaudu (_uow.GetRepository<SettlementPlan>(),_uow.GetRepository<Trip>()), mis ei lae AppUser navigatsioonivarasid — muiduDbSet.Update(plan)ketaks läbi Payments→FromUser/ToUser graafi ja rikuks Identity ridu (ConcurrencyStamp,SecurityStamp). Kui kõik maksed on Confirmed → plan.Status = Completed + plan.CompletedAt; kui reis onFinalizing, läheb see nüüdSettled-iks. Vastasel juhul plan.Status = InProgress.
Reisi elutsükkel (ETripStatus)
Active → Finalizing → Settled (+ Archived). Organisaator klõpsab Finalize Trip — TripService.FinalizeTripAsync lukustab kulude muutmise (iga kulu CRUD kontrollib trip.Status != Active-it) ja kutsub CalculateSettlementAsync-i, mis loob plaani. Reis läheb staatusesse Finalizing (mitte enam otse Settled, nagu varasem versioon tegi). Kui kedagi pole midagi võlgu ja plaani ei looda, läheb reis kohe Settled peale. Settled staatus saavutatakse automaatselt alles siis, kui viimane makse on saaja poolt kinnitatud — see toimub ConfirmPaymentAsync-is. Reopen on lubatud ainult Finalizing seisundis (või tagasiühilduvuse pärast Settled seisundis, kui plaan pole veel Completed); niipea kui mõni makse on juba kinnitatud, ReopenTripAsync tagastab "payments-confirmed" veakoodi.
ExpenseService — kulu loomine koos jaotusega
2 meetodit:
CreateExpenseWithSplitsAsync(...)— loob kulu ja jaotuse (ExpenseSplitkirjed) atomaarselt ühes transaktsioonis. Toetab nelja jagamismeetodit:- EqualAll — võrdselt kõigi aktiivse osaleja vahel.
baseAmount = Math.Floor(total / count * 100) / 100, ülejääk jaotatakse 0.01 kaupa esimestele. - EqualSubset — sama loogika, aga ainult valitud osalejatele.
- ExactAmounts — täpsed summad iga osaleja kohta, otse 1:1 vastendus.
- Percentages —
amount = Math.Round(expense.Amount * percentage / 100, 2), salvestab nii protsendi kui arvutatud summa.
- EqualAll — võrdselt kõigi aktiivse osaleja vahel.
DeleteExpenseWithSplitsAsync(Guid expenseId)— kaskaadne kustutamine: kõigepealt split-id, siis kulu ise.
InvitationService — kutsete haldus
2 meetodit:
CreateInvitationAsync(Guid tripId, Guid userId)— genereerib krüptograafilise tokeni (RandomNumberGenerator.GetBytes(32)= 256 bitti), teisendab URL-ohutuks Base64-ks (asendab+→-,/→_, eemaldab=). Kutse aegub 7 päeva pärast.AcceptInvitationAsync(string token, Guid userId)— valideerib tokeni olemasolu, staatuse (Pending) ja aegumise. Haldab kolme stsenaariumit:- Kasutaja on juba aktiivne osaleja → lihtsalt aktsepteerib kutse
- Kasutaja on mitteaktiivne osaleja → taasaktiveerib (IsActive=true, LeftAt=null)
- Kasutaja pole osaleja → loob uue
TripParticipantkirje Participant rolliga
TripService — reisi loomine
CreateTripAsync(Trip trip, Guid userId)— loob reisi ja esimese osaleja (Organizer rolli) atomaarselt.
PollService — küsitluste haldus
3 meetodit:
CreatePollWithOptionsAsync(TripPoll poll, List<string> optionTexts)— loob küsitluse koos valikuvariantidega (TripPollOptionkirjed koosDisplayOrder-iga). Filtreerib tühjad variandid välja.ToggleVoteAsync(Guid pollId, Guid optionId, Guid userId)— hääletamise toggle-loogika. KuiAllowMultipleVotes = false, eemaldab kõigepealt kasutaja kõik varasemad hääled selles küsitluses. Ei tee midagi, kui küsitlus on suletud (ClosedAt != null).DeletePollCascadeAsync(Guid pollId)— kaskaadne kustutamine: hääled → variandid → küsitlus.
4. DTO-d ja mapperid
DTO-d (Data Transfer Objects)
DTO-d asuvad App.DTO/v1/ kaustas. Need on andmekandjad ilma äriloogikita — neid kasutatakse API sisendiks/väljundiks:
- Response DTO-d:
TripDto,ExpenseDto,BudgetCategoryDto,SettlementPlanDto,SettlementPaymentDto,BalanceDto,SettlementSummaryDto,CurrencyDto,PollDto,PollOptionDto,WishlistItemDto,SplitPresetDto,SplitPresetMemberDto,InvitationDto,TripParticipantDto,ExpenseSplitDto— API tagastab neid, mitte kunagi domeeni entiteete otse - Request DTO-d:
TripCreateDto,TripUpdateDto,ExpenseCreateDto,ExpenseSplitCreateDto,BudgetCategoryCreateDto,PollCreateDto,WishlistItemCreateDto,SplitPresetCreateDto,InvitationCreateDto— API võtab neid vastu kasutajalt - Identity DTO-d:
RegisterInfo,LoginInfo,TokenRefreshInfo,LogoutInfo,JWTResponse— autentimise andmevahetuseks - Vea DTO:
RestApiErrorResponse— standardne veaformaat
Mapperid
Mapperid asuvad App.DTO/Mappers/ kaustas. Need on manuaalsed staatilised klassid (mitte AutoMapper), mis teisendavad domeeni entiteete DTO-deks:
TripMapper,ExpenseMapper,SettlementMapper,CurrencyMapper,BudgetCategoryMapper,InvitationMapper,PollMapper,WishlistMapper,SplitPresetMapper
Kursuse desc.md nõuab: "Manual mappers (no AutoMapper)". Iga mapper on lihtne staatiline meetod, mis kopeerib omadused ühest tüübist teise.
5. Domeeni mudelid (App.Domain)
16 domeeni entiteeti + 3 Identity entiteeti + 8 enum-i. Kõik entiteedid pärivad BaseEntity-lt (Id, CreatedAt, UpdatedAt). BaseEntity genereerib Id automaatselt (Guid.NewGuid()) ja seab ajatemplid UTC-s.
Peamised entiteedid
| Entiteet | Vastutus |
|---|---|
| Trip | Keskne entiteet — reis nimi, sihtkoht, kuupäevad, olek, vaikevaluuta |
| TripParticipant | Seob kasutaja reisiga, roll (Organizer/Participant), unikaalne (TripId, UserId) |
| TripInvitation | Token-põhine kutse reisiga liitumiseks, unikaalne tokeni indeks |
| Expense | Üksik kulutus — summa, maksja, kategooria, jagamismeetod |
| ExpenseSplit | Ühe osaleja osa konkreetses kulus |
| BudgetCategory | Eelarve kategooria reisi-spetsiifiline (nt Food, Transport), nimi on LangStr |
| SplitPreset | Salvestatud jagamise mall (nt "Hotelli grupp") |
| SplitPresetMember | Üks osaleja preset-is |
| SettlementPlan | Arveldusplaan optimeeritud maksetega |
| SettlementPayment | Üks makse arveldusplaanis (kahepoolne kinnitus) |
| Currency | Valuuta referentsandmed (EUR, USD, GBP, SEK, NOK), nimi on LangStr |
| TripWishlistItem | Soovinimekirja element (koht, tegevus, restoran) |
| TripWishlistVote | Hääl soovinimekirja elemendile, unikaalne (WishlistItemId, UserId) |
| TripPoll | Grupi küsitlus otsuste tegemiseks |
| TripPollOption | Küsitluse valikuvariant |
| TripPollVote | Hääl küsitluse valikule, unikaalne (PollOptionId, UserId) |
Identity entiteedid
| Entiteet | Vastutus |
|---|---|
| AppUser | Pärib IdentityUser<Guid>, lisab FirstName ja LastName (max 128) |
| AppRole | Pärib IdentityRole<Guid> |
| AppRefreshToken | JWT refresh token koos rotatsiooniga (eelmine token + aegumisaeg) |
Enum-id
| Enum | Väärtused |
|---|---|
| ETripStatus | Active, Finalizing, Settled, Archived (Finalizing on vahepealne seisund: plaan loodud, maksed käimas, kuid kõik pole veel kinnitatud) |
| ESplitMethod | EqualAll, EqualSubset, ExactAmounts, Percentages |
| EParticipantRole | Organizer, Participant |
| EInvitationStatus | Pending, Accepted, Declined, Expired, Revoked |
| EPaymentStatus | Pending, MarkedPaid, Confirmed |
| ESettlementStatus | Pending, InProgress, Completed |
| EWishlistCategory | Place, Activity, Restaurant, Other |
| EWishlistPriority | MustDo, NiceToHave, Optional |
6. Autentimine ja autoriseerimine
JWT Bearer (API)
- Kasutaja registreerib/logib sisse läbi
POST /api/v1/identity/account/login - Server genereerib JWT tokeni (HS256, claims: userId, rollid, email) ja refresh tokeni
- Klient saadab tokeni iga päringuga:
Authorization: Bearer <token> - ASP.NET middleware valideerib allkirja, aegumist ja väljastajat automaatselt
Refresh token rotatsioon: vana token märgitakse kasutatud ja antakse uus. Vanal tokenil on 1-minutiline üleminekuperiood.
JWT Helper (Base.Helpers)
GenerateJwt(...)— loob JWT tokeniSymmetricSecurityKey+ HMAC-SHA256-gaValidateJWT(...)— valideerib allkirja ja väljastajat, aga mitte aegumist (ValidateLifetime = false) — seda kasutatakse refresh flow's, kus aegunud token on oodatud
Cookie autentimine (MVC)
MVC vaated kasutavad küpsisepõhist autentimist — ASP.NET Identity haldab sessiooni. SlidingExpiration on lubatud.
Rollipõhine autoriseerimine
Süsteemi rollid (Identity): admin, user — kontrollitakse [Authorize(Roles = "admin")] atribuudiga.
Reisi rollid (domeen): Organizer, Participant — kontrollitakse BLL teenustes (nt ITripService.IsOrganizerAsync()), mis omakorda kutsuvad ITripParticipantRepository.IsOrganizerAsync(). Kontrollerid ei pääse repository-le ligi otse.
IDOR kaitse
Iga BLL teenuse meetod, mis puudutab reisi-andmeid, võtab konstruktoris vastu Guid userId parameetri ja kontrollib osaleja/organiseerija staatust teenuse sees. Näiteks TripService.GetByIdWithDetailsAsync(tripId, userId) kutsub esmalt IsParticipantAsync-i — kui false, tagastab null, mida kontroller tõlgib NotFound()/Forbid()-iks. Kuna WebApp kontrollerid ei inject'i IAppUnitOfWork-i (see on Clean-reegel — verifitseeritav grep'iga), kontrollerid ei saa kogemata IDOR-kontrolli vahele jätta — nad peavad alati minema teenuse kaudu, mis kontrolli teeb.
7. ViewModelid
Kursuse nõue on kasutada ViewModele andmete edastamiseks vaadetesse, mitte ViewBag/ViewData'd. Rakenduses on 26 ViewModel klassi.
Admin ViewModelid (AdminViewModels.cs)
Admin alal on 21 ViewModel klassi, mis tagavad järjepideva mustri:
Index ViewModelid (loendite kuvamiseks, filtrite ja otsinguga):
AdminTripIndexViewModel,AdminExpenseIndexViewModel,AdminBudgetCategoryIndexViewModel,AdminSettlementPlanIndexViewModel,AdminTripParticipantIndexViewModel,AdminSettlementPaymentIndexViewModel,AdminPollIndexViewModel,AdminWishlistIndexViewModel,AdminInvitationIndexViewModel,AdminCurrencyIndexViewModel,AdminSplitPresetIndexViewModel
Form ViewModelid (loomine/muutmine koos SelectList-idega):
AdminTripFormViewModel,AdminExpenseFormViewModel,AdminBudgetCategoryFormViewModel,AdminSettlementPlanFormViewModel,AdminTripParticipantFormViewModel,AdminPollFormViewModel,AdminWishlistFormViewModel
Spetsiaalsed ViewModelid:
AdminDashboardViewModel— 23 statistikanumbrit + 3 nimekirja (viimased reisid, kulud, kasutajad)AdminEditRolesViewModel— kasutaja rollide haldamineRoleAssignmentViewModel— abiklass rollide jaoks
Kliendi ViewModelid (kontrollerite failides)
5 ViewModeli on defineeritud otse kontrolleri failides:
| ViewModel | Kontroller | Eesmärk |
|---|---|---|
TripIndexViewModel |
TripsController | Reisi loendi element koos rolliga |
ExpensesIndexViewModel |
ExpensesController | Kulud koos reisi ja valuuta kontekstiga |
BudgetCategoryViewModel |
BudgetController | Kategooria + kulutused + progressiriba arvutused |
SettlementBalanceViewModel |
SettlementController | Kasutaja saldo (makstud vs võlgu + NetBalance) |
WishlistItemViewModel |
WishlistClientController | Soovinimekiri + hääled + kasutaja hääl |
Andmete edastamise mustrid
- Admin ala: 100% ViewModel-põhine, SelectList-id ViewModeli sees
- Kliendi kontrollerid: ViewModeleid kasutatakse peamise andmekandja jaoks; ViewData kasutatakse kontekstandmete jaoks (TripId, TripName, CurrencySymbol, IsOrganizer jne)
- PollsClientController ja MembersController edastavad domeeni entiteete otse (pole eraldi ViewModeli)
8. Tõlked
UI tõlked (.resx failid)
Staatilised tekstid (nupud, sildid, veateated) on .resx failides. Iga fail on kahes keeles:
Shared.resx(inglise) /Shared.et.resx(eesti)Common.resx/Common.et.resx— valideerimisteatedDomain/*.resx— vormiväljanimede ja enum-ide tõlked (Trip, Expense, Currency, BudgetCategory, TripParticipant, TripPoll, TripPollOption, TripWishlistItem, SettlementPlan, SettlementPayment, Enums)
Razor vaadetes: @Localizer["Save"] → "Salvesta" (ET) või "Save" (EN).
Keelevahetaja on navbaris — salvestab keele küpsisesse.
Enum-ide tõlked
EnumHelper (WebApp/Helpers/) kasutab ResourceManager-it enum väärtuste lokaliseerimiseks. Võti: {EnumType}_{Value} (nt ETripStatus_Active), otsitakse App.Resources.Domain.Enums ressursist.
Andmebaasi tõlked (LangStr)
Dünaamiline süsteemne sisu, mida admin haldab, kasutab LangStr — Dictionary<string, string> salvestatakse JSON-ina PostgreSQL-i:
{"en": "Euro", "et": "Euro"}
Currency.Name ja BudgetCategory.Name kasutavad LangStr-i. Admin vormis on kaks inputit (Name EN, Name ET). LangStr.ToString() tagastab automaatselt kasutaja keeles tõlke, fallback-iga neutraalsele kultuurile ja seejärel vaikekultuurile.
Kasutaja-loodud sisu (reisi nimed, kulud, soovinimekirja elemendid) ei kasutata LangStr-i — see on kasutaja enda tekst, mitte süsteemne referentsandmed.
9. API (REST)
Versioonitud: /api/v1/.... Kõik kaitstud endpointid nõuavad JWT Bearer tokenit. Marsruudi muster: /api/v1/[controller]/[action].
Kontrollerid
| Kontroller | Endpointid |
|---|---|
| AccountController | register, login, refreshtoken, logout |
| TripsController | CRUD + osalejate info |
| ExpensesController | CRUD + jagamise loomine |
| BudgetCategoriesController | CRUD reisi kategooriatele |
| InvitationsController | kutse loomine, info, accept/decline/revoke |
| WishlistController | CRUD + hääletus + valmis märkimine |
| PollsController | CRUD + hääletus + sulgemine |
| SettlementsController | saldod, arvelduse arvutamine, mark-paid, confirm |
| SplitPresetsController | CRUD jagamismallidele |
| CurrenciesController | valuutade nimekiri |
Swagger on konfigureeritud JWT Bearer turvameetmega — saab otse brauseris testida tokeniga.
10. MVC veebirakendus
Kliendi kontrollerid (8 tk)
| Kontroller | Peamised tegevused | Autoriseerimismuster |
|---|---|---|
| HomeController | Index, Privacy | Avalik (pole [Authorize]) |
| TripsController | CRUD + detailvaade statistikaga | [Authorize] + osaleja kontroll |
| ExpensesController | CRUD koos 4 jagamismeetodiga | [Authorize] + osaleja kontroll |
| BudgetController | Kategooriate haldamine + progressiribad | [Authorize] + organizer kontroll muutmisteks |
| MembersController | Kutselingi genereerimine, accept, eemaldamine | [Authorize] + organizer kontroll |
| SettlementController | Saldod, makse märkimine, kinnitamine | [Authorize] + osaleja kontroll |
| PollsClientController | Loomine, hääletus, sulgemine | [Authorize] + osaleja kontroll |
| WishlistClientController | CRUD + hääletus + valmis märkimine | [Authorize] + osaleja kontroll |
Reisi kontekstis navigeerimine: Trip Details → nav-grid → Expenses / Budget / Members / Wishlist / Polls / Settlement.
Admin paneel (13 kontrollerit)
Süsteemiadministraatori vaade [Authorize(Roles = "admin")]:
| Kontroller | Vastutus |
|---|---|
| DashboardController | Töölaud statistikaga (AdminDashboardViewModel) |
| TripsController | Kõigi reiside CRUD |
| TripParticipantsController | Osalejate haldamine |
| ExpensesController | Kulude haldamine |
| BudgetCategoriesController | Eelarvekategooriate haldamine |
| CurrenciesController | Valuutade haldamine mitmekeelsete nimedega |
| SettlementPlansController | Arveldusplaanide haldamine |
| SettlementPaymentsController | Maksete jälgimine |
| PollsController | Küsitluste haldamine |
| WishlistController | Soovinimekirja haldamine |
| SplitPresetsController | Jagamismallide haldamine |
| InvitationsController | Kutsete vaatamine ja haldamine |
| UsersController | Kasutajate nimekiri + rollide muutmine |
Admin link navbaris on nähtav ainult kui User.IsInRole("admin") JA kasutaja on sisse logitud.
11. Vaated (Views)
Kliendi vaated (34 tk)
Jagatud kujunduselemendid (Shared/):
_Layout.cshtml— peamine kujundusmall, toast-teated TempData kaudu, tinglik admin-link_LoginPartial.cshtml— sisselogimine/väljalogimine_LanguageSelection.cshtml— keelevahetaja_ValidationScriptsPartial.cshtml— kliendipoolne valideerimineError.cshtml— vealeht
Reisid: Index, Details (dashboard saldo/eelarve ülevaatega), Create, Edit, Delete
Kulud: Index, Create (jagamismeetodi valik + osalejate valik), Edit, Delete
Liikmed: Index, Invite, InviteGenerated (kutselingi kuvamine), AcceptInvitation, InvitationInvalid
Eelarve: Index (progressiribadega), CreateCategory, EditCategory, DeleteCategory
Arveldus: Index (saldod + maksestaatused)
Küsitlused: Index, Create, Details (hääletus + tulemused)
Soovinimekiri: Index, Create, Edit, Delete
Admin vaated (43+ tk)
Iga admin kontroller omab standardset CRUD vaadete komplekti (Index, Details, Create, Edit, Delete). Eraldi:
- Dashboard/Index — statistika
- Users/Index — kasutajate nimekiri
- Users/EditRoles — rollide muutmine
12. Helperid (WebApp/Helpers)
CurrencyConverter
Staatiline klass valuutade teisendamiseks. Olemas kahes kohas: WebApp/Helpers/CurrencyConverter.cs (MVC kontrollerite jaoks) ja App.BLL/Helpers/CurrencyConverter.cs (teenuste jaoks). Mõlemad on identsed.
- Hardcoded kursid EUR baasil: EUR=1.0, USD=0.92, GBP=1.16, SEK=0.087, NOK=0.086
- Teisendus: summa → EUR → sihtvaluuta, ümardamine 2 kohani
- Tundmatu valuuta korral tagastab 1:1 (fallback)
EnumHelper
Staatiline klass enum-väärtuste lokaliseeritud nimede saamiseks:
GetDisplayName<TEnum>(TEnum value)— kasutabResourceManager-it (App.Resources.Domain.Enums)- Võtmeformaat:
{EnumType}_{Value}, fallback: enum väärtuse nimi stringina
InvariantDecimalModelBinderProvider
Custom model binder, mis lubab kasumi sisendites nii punkti (.) kui koma (,) kümnendkoha eraldajana. See lahendab probleemi, kus erinevad brauseri lokaadid saadavad erinevaid formaate.
13. Infrastruktuur
Docker
- Dockerfile — multi-stage build (SDK 10.0 → runtime ASP.NET 10.0), minimeerib image suurust. Port: 8080.
- docker-compose.yml — PostgreSQL 16 + veebirakendus, persistent volume andmebaasile. Hostport: 84 → konteiner 8080.
- Käivitamisel:
docker compose down -v && docker compose up --build
CORS
CorsAllowAll poliitika — lubab kõik päritolud, päised ja meetodid. Eksponeerib päised: X-Version, X-Version-Created-At.
Andmebaas (AppDbContext)
PostgreSQL 16 läbi Npgsql. Konfiguratsioon:
- SplitQuery — väldib karteerianist plahvatust (
UseQuerySplittingBehavior) - NoTrackingWithIdentityResolution — parem jõudlus, aga säilitab entiteetide identiteedi
- Restrict delete behavior — kõik võõrvõtmed, kaskaad teostatud manuaalselt repositorys
- UTC ajatemplid — custom
UtcDateTimeConverterkõigile DateTime omadustele - LangStr JSON —
Currency.NamejaBudgetCategory.Namesalvestatakse JSON-ina - Unikaalsed indeksid — TripInvitation.Token, (TripParticipant.TripId, UserId), küsitlus- ja soovinimekirja hääled
- Automaatsed ajatemplid —
SaveChangesAsync()override uuendabCreatedAt/UpdatedAt
Data Protection
ASP.NET Core Data Protection võtmed salvestatakse andmebaasi (PersistKeysToDbContext).
API versioonimine
Asp.Versioning teek, vaikeversioon 1.0, formaat 'v'VVV (nt v1.0).
Teenuste registreerimine (Program.cs, DI)
Kõik teenused on registreeritud Scoped elutsükliga:
IAppUnitOfWork → AppUnitOfWork
ITripService → TripService
IExpenseService → ExpenseService
ISettlementService → SettlementService
IInvitationService → InvitationService
IPollService → PollService
Marsruutimine
- Admin ala:
{area:exists}/{controller=Dashboard}/{action=Index}/{id?} - Vaikimisi:
{controller=Home}/{action=Index}/{id?} - Razor Pages (Identity UI jaoks)
Andmebaasi initsialiseerimine
Startup ajal (SetupAppData):
- Ootab PostgreSQL ühendust (retry loop)
- Konfiguratsioonist loetavad lipud:
DropDatabase,MigrateDatabase,SeedIdentity,SeedData
Seed andmed
Kasutajad:
- admin@taltech.ee (admin roll)
- user@taltech.ee, alice@taltech.ee, bob@taltech.ee, charlie@taltech.ee, diana@taltech.ee (user roll)
Valuutad: EUR, USD, GBP, SEK, NOK (mitmekeelsete nimedega)
Näidisreisid (4 tk):
- Barcelona Weekend — 4 osalejat, Active, EUR, 11 kulu, eelarve kategooriad, jagamismallid, küsitlus, soovinimekiri
- London Business Trip — 3 osalejat, Settled, GBP, 6 kulu, kinnitatud arveldusplaan
- Summer Cabin Getaway — 5 osalejat, Active, EUR, 7 kulu, pooleliolev arveldus, küsitlus, soovinimekiri, ootel kutse
- NYC Adventure — 3 osalejat, Archived, USD, 9 kulu, suletud küsitlus
14. Staatilised failid ja frontend
CSS
wwwroot/css/site.css— peamine kujundusfailwwwroot/css/splitapp-design.css— SplitApp-spetsiifilised stiilid- Bootstrap 5 (teegi kaust)
JavaScript
wwwroot/js/site.js— saidi skriptidwwwroot/js/splitapp.js— SplitApp-spetsiifilised funktsioonid (toast-teated, jagamismeetodi valik jne)
Teegid (wwwroot/lib/)
- Bootstrap 5, jQuery, Popper.js
15. Projekti failid ja sõltuvused
NuGet paketid (WebApp)
| Pakett | Versioon | Otstarve |
|---|---|---|
| Asp.Versioning.Mvc.ApiExplorer | 8.1.1 | API versioonimine |
| Microsoft.AspNetCore.Authentication.JwtBearer | 10.0.5 | JWT tugi |
| Microsoft.AspNetCore.Identity.EntityFrameworkCore | 10.0.5 | Identity |
| Microsoft.AspNetCore.Identity.UI | 10.0.5 | Identity UI |
| Microsoft.EntityFrameworkCore.Tools | 10.0.5 | EF migratsioonid |
| Npgsql.EntityFrameworkCore.PostgreSQL | 10.0.1 | PostgreSQL tugi |
| Swashbuckle.AspNetCore | 10.1.7 | Swagger/OpenAPI |
Migratsioonid (5 tk)
20260328145416_Initial— esialgne skeem20260328161224_AddBaseEntityTimestamps— CreatedAt/UpdatedAt lisamine20260329141138_CurrencyNameToLangStr— Currency.Name teisendamine LangStr JSON-iks20260402104505_RemoveUnusedBudgetCategoryTranslations— puhastus20260410202112_BudgetCategoryNameToLangStr— BudgetCategory.Name teisendamine LangStr JSON-iks
15. Kaitsmise spikker (defense cheat sheet)
Selle peatüki eesmärk on anda lühikesed, ausad vastused õpetaja tüüpilistele küsimustele.
Küsimus: "Mis arhitektuuri sa kasutasid?"
Vastus: "Clean Architecture'it. Sõltuvused liiguvad sissepoole: WebApp → App.BLL → App.Domain, ning App.DAL.EF on plugin väljaspool, mis implementeerib Domain-interfejse. Repository- ja UoW-liidesed (IAppUnitOfWork, ITripRepository jt) elavad App.Domain/Contracts/-is. App.BLL.csproj ei viita App.DAL.EF-ile üldse — dependency inversion on tagatud Domain-interfejside kaudu. WebApp kontrollerid kasutavad ainult BLL teenuseid — ükski kontroller ei inject'i IAppUnitOfWork-i."
Küsimus: "Kuidas Clean Architecture sinu projektis välja näeb?"
Vastus: "Kolm põhiomadust, mida saab verifitseerida:
- Interfejsid Domain-is:
App.Domain/Contracts/IAppUnitOfWork.cs,ITripRepository.csjne — kokku 11 interfejsi - DAL on plugin:
App.DAL.EF/AppUnitOfWork.csimplementeeribApp.Domain.Contracts.IAppUnitOfWork-i. Sõltuvus liigub DAL → Domain (väljast sisse) - WebApp ei näe DAL-i:
grep IAppUnitOfWork WebApp/Controllers WebApp/ApiControllers WebApp/Areas→ 0 tulemust. DAL-i viidatakse ainult Program.cs-s extension method'i (AddDalServices(connectionString)) kaudu - BLL ei sõltu DAL-ist:
App.BLL.csprojviitab ainultApp.Domain-ile jaApp.DTO-le"
Küsimus: "Kuidas IDOR kaitse töötab?"
Vastus: "IDOR-loogika elab BLL teenustes (ITripService, IExpenseService jt). Iga meetod, mis tagastab või muudab reisi-andmeid, võtab konstruktoris vastu Guid userId parameetri ja kontrollib osaleja/organiseerija staatust teenuse sees. Näiteks TripService.GetByIdWithDetailsAsync(tripId, userId) kutsub esmalt _uow.TripParticipants.IsParticipantAsync(tripId, userId) — kui false, tagastab null. Kontroller tõlgib null → NotFound()/Forbid(). Nii ei saa kontroller kogemata kontrolli vahele jätta, sest kontrollerid ei pääse ligi UoW-le üldse — ainult teenustele."
Küsimus: "Kuidas settlement algoritm töötab?"
Vastus: "Greedy algoritm kahe sorteeritud nimekirjaga. CalculateBalancesAsync arvutab iga osaleja netosaldo (makstud − peab maksma). CalculateSettlementAsync jagab need võlausaldajateks (positiivne saldo) ja võlgnikeks (negatiivne), sorteerib kahanevas järjekorras, ja two-pointer'iga paardab suurima võlausaldaja suurima võlgnikuga. See minimeerib maksete arvu. Lävi 0.01€ väldib ümardamisartefakte. Makse lifecycle: Pending → MarkedPaid (võlgnik märgib, ainult FromUser) → Confirmed (võlausaldaja kinnitab, ainult ToUser). Reisi lifecycle: Active → Finalizing (Finalize vajutusel) → Settled (automaatselt siis, kui viimane makse on kinnitatud — seda teeb ConfirmPaymentAsync plaani all-confirmed kontrollis). Kui kõik maksed Confirmed, plaani staatus Completed ja reis Settled. Tähtis detail: DAL on NoTracking-režiimis, seega iga mutatsioon vajab selget Update()-kutset; plaani uuendamine käib shallow base-repo kaudu, et mitte kaskaadida AppUser navigatsioonivaradesse (ConcurrencyStamp Identity ridu rikuks)."
Küsimus: "Miks mitte AutoMapper?"
Vastus: "Kursuse desc.md nõudis manuaalseid mappereid. Lisaks on manuaalsed mapperid kiiremad (pole reflection'it), debugitavamad (saab breakpointi panna) ja tüübiturvalisemad (kompileerimisaeg error, mitte runtime). DTO struktuurid muutuvad harva, nii et käsitsi kirjutamise vaev on minimaalne."
Küsimus: "Kuidas LangStr töötab andmebaasis?"
Vastus: "LangStr on Dictionary<string, string>, mis serialiseeritakse JSON-ina PostgreSQL-sse. Näiteks Currency.Name on andmebaasis {\"en\":\"Euro\",\"et\":\"Euro\"}. LangStr.ToString() tagastab kasutaja praeguse kultuuri tõlke, fallback'iga neutraalsele kultuurile ja seejärel vaikekultuurile. Admin vormis on kaks eraldi inputit (Name EN, Name ET), mida kontroller paneb kokku LangStr objektiks."
Küsimus: "Milliseid entiteete LangStr kasutab?"
Vastus: "Kaks entiteeti: Currency.Name ja BudgetCategory.Name. Need on süsteemsed referentsandmed, mida admin haldab ja mida kõik kasutajad näevad. Kasutaja-loodud sisu (reisi nimed, kulu kirjeldused, soovinimekirja elemendid) LangStr-i ei kasuta — need on kasutaja enda tekst omas keeles. Nõue oli 'translations in DB', mitte 'every field translated'."
Küsimus: "Miks admin kontrollerites on Admin ViewModelid keerulised?"
Vastus: "Teacher'i nõue oli no viewbags/viewdata - use viewmodels. Lõin kolm põhiklassi:
AdminPageViewModel— baasklassTitleomadusega; iga Index/Form VM pärib selleltAdminDetailsViewModel<T>— geneeriline wrapper Details-vaadetele, et domeeni entiteet ei lekiks otse vaatesseAdminDeleteViewModel<T>— sama Delete jaoks
Admin layout loeb Title-i läbi interface'i cast'i: (Model as ITitledViewModel)?.Title. Seetõttu on admin vaates 0 ViewData/ViewBag kasutust — grep-tööriist kinnitab."
Küsimus: "Kuidas admin Dashboard statistika arvutatakse?"
Vastus: "Kogu agregatsiooniloogika (10+ metrikut, Top Active Trips, Biggest Expenses, User Activity 7d/30d, Activity Feed) elab IAdminStatsService.GetDashboardStatsAsync()-is (App.BLL/Services/Admin/AdminStatsService.cs). Teenus tagastab AdminDashboardData DTO, DashboardController.Index() mappib selle AdminDashboardViewModel-ile ja kuvab vaates. Kontroller ise on ~30 rida — kogu äriloogika on BLL-is, nagu Clean nõuab."
Küsimus: "Kuidas andmebaasi vahetada oleks, kui tahaksid?"
Vastus: "Tänu Clean Architecture'ile väga lihtne. App.Domain/Contracts/ sisaldab kõiki repository-interfejse, App.DAL.EF on nende implementatsioon EF Core + PostgreSQL peal. DB vahetuseks tuleks:
- Luua uus projekt (nt
App.DAL.MongoDB), mis implementeerib samu interfejse - Muuta
Program.csühte rida:builder.Services.AddMongoDalServices(...)asemel praeguseAddDalServices(...) - Migreerida andmed
App.BLL, WebApp ja App.Domain ei muutu — see on Clean-i põhivõit. App.DAL.EF on teadlikult plugin, mida saab asendada."
Küsimus: "Miks JWT refresh token rotatsioon on vajalik?"
Vastus: "Turvalisuse pärast: kui rünnaja varastab vana refresh tokeni, ei saa ta seda kasutada, sest see on juba konkreetse kasutaja uue tokeniga asendatud. Meie implementatsioon: iga refresh-kutse genereerib uue access+refresh paari, vana refresh token märgitakse PreviousToken-iks ja aegub 1 minuti pärast (üleminekuperiood võrgukatkestuste jaoks)."
Küsimus: "Miks CI/CD lükkab ainult main harust?"
Vastus: "Konfigureeritud .gitlab-ci.yml-s only: - main. See takistab juhuslikke feature-branchide deployment'e. Tootmiseks peab explicitly main-i mergima. Docker compose builditakse uuesti iga push'iga, migratsioonid rakendatakse automaatselt (env DataInitialization__MigrateDatabase=true) startup'i ajal."
Küsimus: "Miks 10 enam kui 10 entiteeti?"
Vastus: "Ülesanne nõudis min 10 meaningful. Mul on 16, sest reisikulude domeen on loomulikult rikas: lisaks põhitükkidele (Trip, Expense, User) on vajalikud vote-tabelid (TripPollVote, TripWishlistVote), settlement'i kaks kihti (SettlementPlan → SettlementPayment'id), split-preset'i kaks kihti (SplitPreset → SplitPresetMember), eraldi split-kirjed iga kulu jaoks (ExpenseSplit). Ükski pole trivaalne join-tabel — kõigil on omadused (Amount, Percentage, IsInterested, jne)."
Küsimus: "Mis on kõige keerulisem osa projektis?"
Vastus: "SettlementService.CalculateSettlementAsync() greedy algoritm koos valuutakonversiooniga. Mitu nüansi:
- Iga kulu võib olla erinevas valuutas →
CurrencyConverternormaliseerib reisi vaikevaluutasse - Ümardamisartefaktid (nt 33.33 + 33.33 + 33.34 = 100.00) — lõpliku osaleja summa on floor'itud, ülejääk 0.01 kaupa esimestele
- Two-pointer sorted lists — võlausaldajad kahanevalt, võlgnikud tõusvalt (võlg = negatiivne)
- Makse lifecycle kahepoolse kinnitusega (mark-paid → confirm), mitte lihtsalt 'done'"
Küsimus: "Miks mõni asi jääb Domain-is 'saastunud' (Display atribuudid Resources-ile)?"
Vastus: "Teadlik pragmaatiline kompromiss. App.Domain/*.cs entiteetidel on jätkuvalt [Display(ResourceType = typeof(App.Resources.Domain.Trip))] atribuudid, mis seovad Domain-i Resources-iga. Täielikus Cleanis oleks need atribuudid DTO-des või ViewModelides. Ma teadlikult ei kolinud neid, sest see oleks katkestanud ModelState valideerimise ja nõudnud iga form'i re-testi. Kõik muud Clean-põhimõtted (interfejsid Domain-is, DAL plugin, BLL ↛ DAL, WebApp ↛ UoW) on rangelt järgitud."
Küsimus: "Kuidas sõltuvuse inversioon sinu projektis konkreetselt toimib?"
Vastus: "Konkreetne näide. App.BLL/Services/TripService.cs deklareerib:
using App.Domain.Contracts; // interfejs Domain-ist
public class TripService : ITripService {
private readonly IAppUnitOfWork _uow; // Domain-interfejs
public TripService(IAppUnitOfWork uow) { _uow = uow; }
}
BLL ei tea App.DAL.EF-ist midagi. Kompileerimise ajal pole App.BLL.csproj-s DAL-i viidet. DI-container ühendab käivitamisel IAppUnitOfWork → AppUnitOfWork (DAL-ist) tänu Program.cs AddDalServices() registreerimisele. See ongi dependency inversion — kõrgem kiht (BLL) sõltub abstraktsioonist (Domain), mitte konkreetsest implementatsioonist (DAL)."
16. Mida võiks paremini teha
Ausalt — kohad, kus projekt võiks olla parem:
- Domain puhastamine —
App.Domain/*.csentiteetidel on jätkuvalt[Display(ResourceType = typeof(App.Resources.Domain.X))]atribuudid. Täielikus Cleanis peaksid need olema DTO-del või ViewModelidel. Teadlik pragmaatiline kompromiss ModelState-valideerimise tõttu. - LangStr laiem kasutus — praegu ainult 2 entiteedis (
Currency.Name,BudgetCategory.Name). Võiks laienedaTrip.Name,TripPoll.Question,SplitPreset.Namepeale. - Integratsioontestid — ükshaaval tehtud manuaalne testimine; CI käigus võiks olla
dotnet testkoos in-memory andmebaasiga. Clean Architecture teeb testide kirjutamise lihtsamaks (teenuseid saab mockida läbi Domain-interfejside). - Valuutakursid — praegu hardcoded
CurrencyConverter-is. Reaalses rakenduses peaks need tulema välisest API-st. - Rate limiting — puudub. API endpointid on kaitsmata DDoS-i eest.
- Logimine — lihtne
Console.WriteLinemitmes kohas (eritiSetupAppData). Structured logging Serilog-iga oleks parem. - WebApp → DAL kompromissviide —
WebApp.csprojviitab endiseltApp.DAL.EF-ile, etProgram.cssaaks kutsudaAddDalServices(). 100% isolatsiooniks oleks vaja eraldiApp.DAL.EF.Bootstrapprojekti, mis on Cleani purist'i jaoks väärt, aga praktiliselt over-engineering.
Need ei ole puuduvad nõuded — need on parandusvõimalused.