profileShare

rasmusjy / splitapp-backend-clean-onion

Read-only snapshot

No repository description.

main default branch 429 files Expires Sep 13, 2026, 9:06 AM
explanation.md 46,987 bytes

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.ymldocker 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 (SettlementService greedy algoritm, ExpenseService 4 split-meetodit, InvitationService token-põhised kutsed, PollService hää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:

  • IAppUnitOfWork ja 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.csproj ei viita enam App.DAL.EF-ile — ainult App.Domain-ile ja App.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 tulemust
  • grep "IAppUnitOfWork\|_uow\." WebApp/Controllers/ WebApp/ApiControllers/ WebApp/Areas/0 tulemust
  • App.BLL.csproj refs: ainult App.Domain, App.DTO
  • App.DAL.EF viitab Domain-i interfejsidele ja implementeerib neid (AppUnitOfWork : IAppUnitOfWork Domain-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 userId parameetri 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 toimub SaveChangesAsync() 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 seostega
  • RemoveAsync(Guid id)override, mis teostab kaskaadse kustutamise õiges järjekorras (lapselapsed → lapsed → reis), kuna kõik võõrvõtmed on DeleteBehavior.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. Kasutab CurrencyConverter-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). Loob SettlementPayment kirjed staatusega Pending.

  • MarkPaidAsync(Guid paymentId, Guid userId) — võlgnik märgib makse tehtuks. Muudab staatust Pending → MarkedPaid, salvestab kuupäeva. Ainult FromUserId saab seda teha. Guarded variant MarkPaidGuardedAsync kontrollib 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. Ainult ToUserId saab seda teha (guarded variant ConfirmPaymentGuardedAsync jõustab selle). Pärast mutatsiooni laaditakse plaan SettlementPlanRepository.GetByIdAsync-iga (koos Payments-iga), et kontrollida kas kõik maksed on kinnitatud. NB! DAL on konfigureeritud QueryTrackingBehavior.NoTrackingWithIdentityResolution-iga — iga laetud entiteet on detached, mistõttu mutatsioonide salvestamiseks on vajalik selgesõnaline paymentRepo.Update(payment) kutse (vastasel juhul SaveChangesAsync ei 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 — muidu DbSet.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 on Finalizing, läheb see nüüd Settled-iks. Vastasel juhul plan.Status = InProgress.

Reisi elutsükkel (ETripStatus)

Active → Finalizing → Settled (+ Archived). Organisaator klõpsab Finalize TripTripService.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 (ExpenseSplit kirjed) 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.
    • Percentagesamount = Math.Round(expense.Amount * percentage / 100, 2), salvestab nii protsendi kui arvutatud summa.
  • 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:

    1. Kasutaja on juba aktiivne osaleja → lihtsalt aktsepteerib kutse
    2. Kasutaja on mitteaktiivne osaleja → taasaktiveerib (IsActive=true, LeftAt=null)
    3. Kasutaja pole osaleja → loob uue TripParticipant kirje 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 (TripPollOption kirjed koos DisplayOrder-iga). Filtreerib tühjad variandid välja.

  • ToggleVoteAsync(Guid pollId, Guid optionId, Guid userId) — hääletamise toggle-loogika. Kui AllowMultipleVotes = 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)

  1. Kasutaja registreerib/logib sisse läbi POST /api/v1/identity/account/login
  2. Server genereerib JWT tokeni (HS256, claims: userId, rollid, email) ja refresh tokeni
  3. Klient saadab tokeni iga päringuga: Authorization: Bearer <token>
  4. 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 tokeni SymmetricSecurityKey + HMAC-SHA256-ga
  • ValidateJWT(...) — 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 haldamine
  • RoleAssignmentViewModel — 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 — valideerimisteated
  • Domain/*.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 LangStrDictionary<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 valideerimine
  • Error.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) — kasutab ResourceManager-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 UtcDateTimeConverter kõigile DateTime omadustele
  • LangStr JSONCurrency.Name ja BudgetCategory.Name salvestatakse JSON-ina
  • Unikaalsed indeksid — TripInvitation.Token, (TripParticipant.TripId, UserId), küsitlus- ja soovinimekirja hääled
  • Automaatsed ajatemplidSaveChangesAsync() override uuendab CreatedAt/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

  1. Admin ala: {area:exists}/{controller=Dashboard}/{action=Index}/{id?}
  2. Vaikimisi: {controller=Home}/{action=Index}/{id?}
  3. 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:

Valuutad: EUR, USD, GBP, SEK, NOK (mitmekeelsete nimedega)

Näidisreisid (4 tk):

  1. Barcelona Weekend — 4 osalejat, Active, EUR, 11 kulu, eelarve kategooriad, jagamismallid, küsitlus, soovinimekiri
  2. London Business Trip — 3 osalejat, Settled, GBP, 6 kulu, kinnitatud arveldusplaan
  3. Summer Cabin Getaway — 5 osalejat, Active, EUR, 7 kulu, pooleliolev arveldus, küsitlus, soovinimekiri, ootel kutse
  4. NYC Adventure — 3 osalejat, Archived, USD, 9 kulu, suletud küsitlus

14. Staatilised failid ja frontend

CSS

  • wwwroot/css/site.css — peamine kujundusfail
  • wwwroot/css/splitapp-design.css — SplitApp-spetsiifilised stiilid
  • Bootstrap 5 (teegi kaust)

JavaScript

  • wwwroot/js/site.js — saidi skriptid
  • wwwroot/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)

  1. 20260328145416_Initial — esialgne skeem
  2. 20260328161224_AddBaseEntityTimestamps — CreatedAt/UpdatedAt lisamine
  3. 20260329141138_CurrencyNameToLangStr — Currency.Name teisendamine LangStr JSON-iks
  4. 20260402104505_RemoveUnusedBudgetCategoryTranslations — puhastus
  5. 20260410202112_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:

  1. Interfejsid Domain-is: App.Domain/Contracts/IAppUnitOfWork.cs, ITripRepository.cs jne — kokku 11 interfejsi
  2. DAL on plugin: App.DAL.EF/AppUnitOfWork.cs implementeerib App.Domain.Contracts.IAppUnitOfWork-i. Sõltuvus liigub DAL → Domain (väljast sisse)
  3. 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
  4. BLL ei sõltu DAL-ist: App.BLL.csproj viitab ainult App.Domain-ile ja App.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 nullNotFound()/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 — baasklass Title omadusega; iga Index/Form VM pärib sellelt
  • AdminDetailsViewModel<T> — geneeriline wrapper Details-vaadetele, et domeeni entiteet ei lekiks otse vaatesse
  • AdminDeleteViewModel<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:

  1. Luua uus projekt (nt App.DAL.MongoDB), mis implementeerib samu interfejse
  2. Muuta Program.cs ühte rida: builder.Services.AddMongoDalServices(...) asemel praeguse AddDalServices(...)
  3. 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:

  1. Iga kulu võib olla erinevas valuutas → CurrencyConverter normaliseerib reisi vaikevaluutasse
  2. Ümardamisartefaktid (nt 33.33 + 33.33 + 33.34 = 100.00) — lõpliku osaleja summa on floor'itud, ülejääk 0.01 kaupa esimestele
  3. Two-pointer sorted lists — võlausaldajad kahanevalt, võlgnikud tõusvalt (võlg = negatiivne)
  4. 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 IAppUnitOfWorkAppUnitOfWork (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:

  1. Domain puhastamineApp.Domain/*.cs entiteetidel 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.
  2. LangStr laiem kasutus — praegu ainult 2 entiteedis (Currency.Name, BudgetCategory.Name). Võiks laieneda Trip.Name, TripPoll.Question, SplitPreset.Name peale.
  3. Integratsioontestid — ükshaaval tehtud manuaalne testimine; CI käigus võiks olla dotnet test koos in-memory andmebaasiga. Clean Architecture teeb testide kirjutamise lihtsamaks (teenuseid saab mockida läbi Domain-interfejside).
  4. Valuutakursid — praegu hardcoded CurrencyConverter-is. Reaalses rakenduses peaks need tulema välisest API-st.
  5. Rate limiting — puudub. API endpointid on kaitsmata DDoS-i eest.
  6. Logimine — lihtne Console.WriteLine mitmes kohas (eriti SetupAppData). Structured logging Serilog-iga oleks parem.
  7. WebApp → DAL kompromissviideWebApp.csproj viitab endiselt App.DAL.EF-ile, et Program.cs saaks kutsuda AddDalServices(). 100% isolatsiooniks oleks vaja eraldi App.DAL.EF.Bootstrap projekti, mis on Cleani purist'i jaoks väärt, aga praktiliselt over-engineering.

Need ei ole puuduvad nõuded — need on parandusvõimalused.