README.md
15,281 bytes
| 1 | # SplitApp — Trip Expense Management (Microservices) |
|---|---|
| 2 | |
| 3 | SplitApp is an ASP.NET Core 10.0 web application for managing group trips and splitting expenses. Users create trips, invite friends, track costs with flexible splitting (equal / equal-subset / exact / percentage), manage budgets, run polls, maintain a wishlist, and settle debts via an optimized algorithm. |
| 4 | |
| 5 | This repo is the **microservices** implementation. It builds on the modular-monolith version (`splitapp-backend-modular-monolith`) by extracting the **Users module into a separate microservice** (`SplitApp.UsersService`) with its own database. The two processes communicate via **RabbitMQ** (inter-service RPC + events) and **HTTP REST** (login / register / admin CRUD). Runs locally via `docker compose` (webapp + users-service + RabbitMQ + two Postgres databases). |
| 6 | |
| 7 | --- |
| 8 | |
| 9 | ## Run |
| 10 | |
| 11 | ```bash |
| 12 | docker compose -p splitapp-phase4 up --build -d |
| 13 | ``` |
| 14 | |
| 15 | Brings up five containers: |
| 16 | |
| 17 | | Service | Container | Host port | Notes | |
| 18 | |---|---|---|---| |
| 19 | | `webapp` | `phase4-webapp` | http://localhost:97 | The MVC monolith (Trips + Expenses + Admin UX) | |
| 20 | | `users-service` | `phase4-users-service` | http://localhost:98 | The Users microservice — Identity, JWT issuance, admin REST API | |
| 21 | | `rabbitmq` | `phase4-rabbitmq` | http://localhost:15672 (UI), :5672 | Message broker (guest / guest) | |
| 22 | | `db-monolith` | `phase4-db-monolith` | (internal) | PostgreSQL 16, `splitapp` database (Trips + Expenses) | |
| 23 | | `db-users` | `phase4-db-users` | (internal) | PostgreSQL 16, `splitapp_users` database (Identity + RefreshTokens) | |
| 24 | |
| 25 | Module migrations run automatically on each service's startup. The `users-service` seeds the 6 test users; the `webapp` skips example-trip seeding (the Users service doesn't expose seed data without admin auth — see "Known limitations" below). |
| 26 | |
| 27 | Test login (after seed): |
| 28 | - `user@example.com`, `alice@example.com`, `bob@example.com`, `charlie@example.com`, `diana@example.com` / `Kala.12345` — regular users. The password is in the source on purpose: this is a demo and the data is invented. |
| 29 | - The `admin` account is seeded only when `SEED_ADMIN_PASSWORD` is set, and there is no default. Without the variable there is no administrator at all. |
| 30 | |
| 31 | Stop: |
| 32 | |
| 33 | ```bash |
| 34 | docker compose -p splitapp-phase4 down # keep data volumes |
| 35 | docker compose -p splitapp-phase4 down -v # also drop the named volumes |
| 36 | ``` |
| 37 | |
| 38 | --- |
| 39 | |
| 40 | ## Architecture at a glance |
| 41 | |
| 42 | ``` |
| 43 | ┌──────────────────────┐ HTTP REST (login/register/admin) ┌──────────────────────┐ |
| 44 | │ SplitApp.WebApp │ ◄──────────────────────────────────► │ SplitApp.UsersService│ |
| 45 | │ (monolith, port 97) │ │ (microservice, 98) │ |
| 46 | │ │ RabbitMQ (RPC + events, JSON) │ │ |
| 47 | │ - Trips module │ ◄──────────────────────────────────► │ - Users.Domain │ |
| 48 | │ - Expenses module │ │ - Users.Application │ |
| 49 | │ - MVC client UX │ │ - Users.Infrastructure│ |
| 50 | │ - Full Admin UX │ │ - Users.Api (REST) │ |
| 51 | │ - JWT (validate) │ │ - Identity + UoW │ |
| 52 | │ - No Users.* refs │ │ - JWT issuer │ |
| 53 | └──────────┬───────────┘ └───────────┬───────────┘ |
| 54 | │ │ |
| 55 | ▼ ▼ |
| 56 | ┌───────────────┐ ┌───────────────┐ |
| 57 | │ db-monolith │ ┌──────────────┐ │ db-users │ |
| 58 | │ (Postgres 16) │ │ rabbitmq │ │ (Postgres 16) │ |
| 59 | │ trips + exp. │ │ exchanges: │ │ Identity + │ |
| 60 | │ │ │ splitapp. │ │ refresh tokens│ |
| 61 | │ │ │ events │ │ │ |
| 62 | │ │ │ splitapp. │ │ │ |
| 63 | │ │ │ requests │ │ │ |
| 64 | └───────────────┘ └──────────────┘ └───────────────┘ |
| 65 | ``` |
| 66 | |
| 67 | **Communication contract:** |
| 68 | |
| 69 | | Concern | Channel | Direction | |
| 70 | |---|---|---| |
| 71 | | Login / Register / Refresh / Logout | HTTP `/api/v1/identity/account/*` | WebApp → UsersService | |
| 72 | | Admin user list / edit / delete / roles | HTTP `/api/v1/identity/admin/*` | WebApp → UsersService | |
| 73 | | Inter-module user lookup (id → DisplayName/Email) | RabbitMQ RPC (`GetUserByIdRequest`, `GetUsersByIdsRequest`) | WebApp → UsersService | |
| 74 | | User deletion fan-out | RabbitMQ event (`UserDeletedEvent`) | UsersService → WebApp | |
| 75 | | JWT validation | Local (shared HS256 key in config) | both services validate identically | |
| 76 | |
| 77 | **Reference rules:** |
| 78 | - `SplitApp.WebApp` has **zero project references** to any `SplitApp.Modules.Users.*` project. |
| 79 | - Cross-process inter-module function calls go through **RabbitMQ only** (via the `IUserLookup` abstraction in `SplitApp.Shared.Messaging`). |
| 80 | - `Trips.Api` + `Expenses.Api` controllers + `AppUnitOfWork` use `IUserLookup` (RabbitMQ RPC), not in-process MediatR, for user data. |
| 81 | - Inside the monolith (Trips ↔ Expenses), MediatR is still used in-process for cross-module calls — only **Users** moved out of process. |
| 82 | |
| 83 | See [architecture.md](architecture.md) for the full architecture deep-dive. |
| 84 | |
| 85 | --- |
| 86 | |
| 87 | ## Solution layout |
| 88 | |
| 89 | ``` |
| 90 | SplitApp.Modular/ |
| 91 | ├── SplitApp.sln |
| 92 | ├── Directory.Build.props |
| 93 | ├── src/ |
| 94 | │ ├── SplitApp.WebApp/ ← monolith host (Trips + Expenses + MVC + Admin) |
| 95 | │ │ ├── Program.cs ← JWT bearer + cookie reader, AddMessaging, AddHttpClient<IUsersServiceClient> |
| 96 | │ │ ├── Application/ |
| 97 | │ │ │ ├── Services/ (+ Admin/) ← lifted phase-2 BLL — IUsersServiceClient for user lookups |
| 98 | │ │ │ ├── DTO/ ← BllDtos (no AppUser refs anywhere) |
| 99 | │ │ │ ├── Mappers/ ← Domain ↔ BllDto factories (UserDto-based) |
| 100 | │ │ │ ├── Persistence/AppUnitOfWork.cs ← Trips + Expenses DbContexts, IUserLookup for user hydration |
| 101 | │ │ │ ├── Persistence/CrossModuleNavigationLoader.cs ← uses IUserLookup (RabbitMQ RPC) |
| 102 | │ │ │ ├── UsersService/ ← typed HttpClient + JwtForwardingHandler |
| 103 | │ │ │ └── Messaging/UserDeletedEventHandler.cs ← RabbitMQ event subscriber |
| 104 | │ │ ├── Controllers/AccountController.cs ← MVC login/register/logout (replaces Areas/Identity) |
| 105 | │ │ ├── Areas/Admin/ ← full admin UX, UsersController calls HTTP REST |
| 106 | │ │ └── Resources/ ← i18n .resx (EN + ET) |
| 107 | │ ├── Services/Users/SplitApp.UsersService/ ← NEW microservice host |
| 108 | │ │ ├── Program.cs ← AddUsersModule + MassMessaging consumers + Swagger |
| 109 | │ │ ├── Messaging/ ← GetUserByIdRequestHandler, GetUsersByIdsRequestHandler |
| 110 | │ │ ├── Controllers/HealthController.cs ← /health endpoint (gated on seed completion) |
| 111 | │ │ └── Hosting/ ← Swagger config + SeededHealthState |
| 112 | │ ├── Shared/ |
| 113 | │ │ ├── SplitApp.Shared.Kernel/ ← BaseEntity, IBaseRepo, IUoW, LangStr, IdentityHelpers |
| 114 | │ │ ├── SplitApp.Shared.Contracts/ ← in-process MediatR contracts (Trips ↔ Expenses) |
| 115 | │ │ └── SplitApp.Shared.Messaging/ ← RabbitMQ wrapper, IUserLookup, integration messages |
| 116 | │ └── Modules/ |
| 117 | │ ├── Users/ ← consumed by SplitApp.UsersService (NOT by WebApp) |
| 118 | │ ├── Trips/ ← in-process module in WebApp |
| 119 | │ └── Expenses/ ← in-process module in WebApp |
| 120 | └── tests/ |
| 121 | ├── SplitApp.Modules.Users.Tests/ |
| 122 | ├── SplitApp.Modules.Trips.Tests/ |
| 123 | ├── SplitApp.Modules.Expenses.Tests/ |
| 124 | ├── SplitApp.Shared.Messaging.Tests/ ← NEW: integration message contract tests |
| 125 | └── SplitApp.WebApp.IntegrationTests/ ← architecture invariants + HTTP smoke |
| 126 | ``` |
| 127 | |
| 128 | The `Modules/Users/*` projects still exist in the modular monolith layout — but they are consumed only by the new `SplitApp.UsersService` host. The `SplitApp.WebApp.csproj` has zero `<ProjectReference>` entries pointing at any `Users.*` project. |
| 129 | |
| 130 | --- |
| 131 | |
| 132 | ## URL map |
| 133 | |
| 134 | ### Webapp (port 97) |
| 135 | | URL | Purpose | |
| 136 | |---|---| |
| 137 | | `/` | Landing page (MVC) | |
| 138 | | `/Account/{Login, Register, Logout, Manage}` | MVC auth (HTTP → Users service → JWT cookie) | |
| 139 | | `/Trips`, `/Trips/{Create,Details/{id},Edit/{id},Delete/{id}}` | Trip CRUD | |
| 140 | | `/Members?tripId={id}` and `/Members/AcceptInvitation/{token}` | Trip participants + invitation flow | |
| 141 | | `/Expenses?tripId={id}` (with Create/Edit/Delete) | Trip expenses | |
| 142 | | `/Budget?tripId={id}` (CreateCategory/EditCategory/DeleteCategory) | Budget categories | |
| 143 | | `/Settlement?tripId={id}` | Balances + settlement plans | |
| 144 | | `/PollsClient?tripId={id}` (Create/Details) | Trip polls | |
| 145 | | `/WishlistClient?tripId={id}` | Trip wishlist | |
| 146 | | `/Admin/Dashboard` | Admin home (`admin` role) | |
| 147 | | `/Admin/Users` | Admin user list — proxies to `users-service` via HTTP | |
| 148 | | `/Admin/{Trips, Expenses, BudgetCategories, Currencies, Invitations, Polls, SettlementPlans, SettlementPayments, SplitPresets, TripParticipants, Wishlist}` | Admin CRUD per entity | |
| 149 | | `/swagger` | Swagger UI — Trips + Expenses REST only | |
| 150 | |
| 151 | ### Users service (port 98) |
| 152 | | URL | Purpose | |
| 153 | |---|---| |
| 154 | | `/swagger` | Swagger UI for all Users REST endpoints | |
| 155 | | `/health` | Healthcheck (200 only after migrate + seed completes) | |
| 156 | | `/api/v1/identity/account/{register, login, logout, refreshtokendata}` | Auth (anonymous) | |
| 157 | | `/api/v1/identity/admin/users` (GET/PUT/DELETE/+ `/{id}/roles`, `/admin/roles`) | Admin user CRUD (`[Authorize(Roles = "admin")]`) | |
| 158 | |
| 159 | ### RabbitMQ management (port 15672) |
| 160 | - Login `guest` / `guest` |
| 161 | - Exchanges: `splitapp.events` (topic), `splitapp.requests` (direct) |
| 162 | - Queues: `q.webapp.UserDeletedEvent`, `q.users-service.GetUserByIdRequest`, `q.users-service.GetUsersByIdsRequest` |
| 163 | |
| 164 | --- |
| 165 | |
| 166 | ## Inter-service messaging |
| 167 | |
| 168 | All cross-service communication goes through the `SplitApp.Shared.Messaging` library. Two patterns: |
| 169 | |
| 170 | | Pattern | Used for | Library API | |
| 171 | |---|---|---| |
| 172 | | **Pub/Sub (events)** | Fire-and-forget fan-out (`UserDeletedEvent`) | `IMessageBus.PublishEventAsync<T>` + `IEventHandler<T>` | |
| 173 | | **RPC (requests)** | Synchronous data lookups (`GetUserByIdRequest`, `GetUsersByIdsRequest`) | `IMessageBus.RequestAsync<TReq, TResp>` + `IRequestHandler<TReq, TResp>` | |
| 174 | |
| 175 | For the WebApp, `IUserLookup` is the high-level facade over the bus — controllers and services consume `IUserLookup`, not `IMessageBus` directly. |
| 176 | |
| 177 | In-process MediatR is still used between **Trips ↔ Expenses** modules (they live in the same process). Only the Users module crossed the process boundary. |
| 178 | |
| 179 | --- |
| 180 | |
| 181 | ## Tests |
| 182 | |
| 183 | ```bash |
| 184 | cd SplitApp.Modular |
| 185 | dotnet test |
| 186 | ``` |
| 187 | |
| 188 | **53 tests** across five projects, all passing: |
| 189 | |
| 190 | | Project | Tests | Covers | |
| 191 | |---|---:|---| |
| 192 | | `SplitApp.Modules.Users.Tests` | 8 | `IdentityHelpers` — JWT generation/validation round-trips | |
| 193 | | `SplitApp.Modules.Trips.Tests` | 12 | `LangStr` — multi-language fallback, edge cases | |
| 194 | | `SplitApp.Modules.Expenses.Tests` | 10 | `CurrencyConverter` — exchange rates, rounding | |
| 195 | | `SplitApp.Shared.Messaging.Tests` | 9 | Wire-format contract round-trips (`GetUserByIdRequest`, `UserDeletedEvent`, `UserDto`, …) + marker interface invariants + queue naming | |
| 196 | | `SplitApp.WebApp.IntegrationTests` | 14 | Architecture invariants (module boundaries, schema isolation) + `WebApplicationFactory<Program>` HTTP smoke | |
| 197 | |
| 198 | --- |
| 199 | |
| 200 | ## Phase 4 ↔ Phase 3 mapping |
| 201 | |
| 202 | | Phase 3 (modular monolith) | Phase 4 (microservices) | |
| 203 | |---|---| |
| 204 | | 1 process (`SplitApp.WebApp`) | 2 processes (`SplitApp.WebApp` + `SplitApp.UsersService`) | |
| 205 | | 1 Postgres database, 3 schemas (`users`/`trips`/`expenses`) | 2 Postgres databases (`splitapp` + `splitapp_users`) | |
| 206 | | In-process MediatR for all cross-module calls | RabbitMQ (`Users.*`) + in-process MediatR (`Trips`/`Expenses`) | |
| 207 | | `UsersDbContext` registered in `WebApp` host | `UsersDbContext` registered in `SplitApp.UsersService` only | |
| 208 | | `Areas/Identity` scaffolded Razor Pages handle login/register | New `Controllers/AccountController.cs` posts to Users service via HTTP, stores JWT in HttpOnly cookie | |
| 209 | | `Admin/UsersController` uses `UserManager<AppUser>` directly | `Admin/UsersController` uses typed `IUsersServiceClient` HTTP client | |
| 210 | | `[NotMapped] AppUser? CreatedBy` on Trip/Expense entities | `[NotMapped] UserDto? CreatedBy` — entities no longer reference Users domain types | |
| 211 | | ~13 WebApp files inject `UserManager`/`SignInManager`/`RoleManager` | All replaced with `User.FindFirstValue(ClaimTypes.NameIdentifier)` claim reads | |
| 212 | | 44 tests | 53 tests (added Shared.Messaging.Tests) | |
| 213 | | One container deploy (`docker compose -p splitapp-phase3 up`) | Five-container compose with healthcheck-gated startup | |
| 214 | |
| 215 | --- |
| 216 | |
| 217 | ## Known limitations |
| 218 | |
| 219 | - **No example trip data on first boot.** `AppDataInit.SeedExampleData` calls `IUsersServiceClient.ListUsersAsync()` without a JWT (it runs from startup, not an HTTP request context). The Users service returns 401, the `try`/`catch` swallows it, and example trips don't get seeded. Core user accounts (admin, alice, …) ARE seeded by the Users service itself, so login works — you just create your own trips from scratch. |
| 220 | - **Graceful degradation if RabbitMQ is unavailable.** `IUserLookup` catches `MessageBusTimeout`/`MessageBusUnavailableException` and returns empty results, so pages render with blank user names rather than crashing. Logs show the warning. |
| 221 | |
| 222 | --- |
| 223 | |
| 224 | ## Files & docs |
| 225 | |
| 226 | - [architecture.md](architecture.md) — architecture deep-dive |
| 227 | - [SplitApp.Modular/README.md](SplitApp.Modular/README.md) — solution-level README |
| 228 | |