profileShare

rasmusjy / splitapp-backend-microservices

Read-only snapshot

No repository description.

main default branch 501 files Expires Sep 13, 2026, 9:06 AM
README.md 5,607 bytes

SplitApp — Phase 3 Modular Monolith

ASP.NET Core 10 modular monolith for splitting trip expenses among participants. Three modules (Users, Trips, Expenses) communicating via MediatR; one deployable; per-module Postgres schemas. Full phase-2 UI surface preserved (101 Razor views, 21 MVC + Admin controllers, identity Razor pages, 10 REST API controllers).

See docs/ARCHITECTURE.md for layout, reference rules, contracts, per-module migration commands, and the [NotMapped] cross-module navigation caveat.

Run

With Docker (recommended)

From the repo root (one level up from this directory):

docker compose up --build

Deployment: runs locally via Docker Compose (see the repo root README).

Locally, phase 3 listens on http://localhost:90 (host port 90 → container port 8080). Each module's migrations are applied automatically on startup. The container is named phase3. Postgres uses dedicated schemas users / trips / expenses inside the same splitapp database (DB is internal-only — not exposed to host).

The single Dockerfile lives at the repo root and copies from SplitApp.Modular/. There is no separate Dockerfile inside this directory — the root one is canonical.

Without Docker

docker compose up -d db
dotnet run --project src/SplitApp.WebApp

By default dotnet run uses https://localhost:7133 / http://localhost:5297 (see Properties/launchSettings.json).

URL map

URL Purpose
/ Landing page (MVC)
/Trips, /Trips/Create, /Trips/Details/{id}, /Trips/Edit/{id}, /Trips/Delete/{id} Trip CRUD
/Members?tripId={id} and /Members/AcceptInvitation/{token} Trip participants + invitation flow
/Expenses?tripId={id} (with Create/Edit/Delete) Trip expenses
/Budget?tripId={id} (with CreateCategory/EditCategory/DeleteCategory) Budget categories per trip
/Settlement?tripId={id} Balances + settlement plans
/PollsClient?tripId={id} (Create/Details) Trip polls
/WishlistClient?tripId={id} (Create/Edit/Delete) Trip wishlist
/Identity/Account/Register Cookie-based register
/Admin/Dashboard Admin home (admin role)
/Admin/{Users, Trips, Expenses, BudgetCategories, Currencies, Invitations, Polls, SettlementPlans, SettlementPayments, SplitPresets, TripParticipants, Wishlist} Admin CRUD over each entity
/swagger Swagger UI listing every module's REST endpoints

REST API

Module Endpoints
Users /api/v1/identity/account/{register, login, logout, refreshtokendata}
Trips /api/v1/trips, /api/v1/budgetcategories, /api/v1/invitations, /api/v1/polls, /api/v1/wishlist
Expenses /api/v1/expenses, /api/v1/currencies, /api/v1/settlements, /api/v1/splitpresets

Tests

dotnet test

25 tests across four projects:

  • Per-module unit testsCurrencyConverter (5), LangStr (7), IdentityHelpers JWT round-trips (4)
  • Architecture invariants (in tests/SplitApp.WebApp.IntegrationTests/Architecture/):
    • ModuleBoundaryTests — no module's Application/Infrastructure/Api may <ProjectReference> another module
    • DbContextSchemaIsolationTests — every DbContext exposes DbSet<T> only for entities in its own Domain project
    • CrossModuleNavigationTests — cross-module navigation properties allowed only when [NotMapped]; the WebApp facade hydrates them in-memory after loading from the owning module's DbContext, so EF never crosses schemas
  • SmokeWebApplicationFactory<Program> boots the full host in Testing env (skipping migrations) and serves /, /Home/Index, returns 401 on unauthenticated API hit

A failing architecture test means a developer just violated the modular-monolith invariant.

Module owners

Module Owns Schema
Users Identity, JWT issuance, refresh tokens, user profile users
Trips Trips, participants, invitations, polls, wishlists, budget categories trips
Expenses Expenses, splits, settlement plans, settlement payments, split presets, currencies expenses

Caveat — [NotMapped] cross-module navigation properties

To preserve phase 2's view-rendering parity (BLL DTO factories that read Trip.DefaultCurrency.Code, TripParticipant.User.FirstName, etc.) the entity classes still declare those navigation properties — annotated [NotMapped] so EF never crosses schemas. The CrossModuleNavigationLoader in src/SplitApp.WebApp/Application/Persistence/ populates them in C# after entity load by querying the appropriate module's DbContext.

Keeping the property types on the entities required adding <ProjectReference> between Domain projects:

Modules/Trips/SplitApp.Modules.Trips.Domain
   → Modules/Users/SplitApp.Modules.Users.Domain (for AppUser refs)
   → Modules/Expenses/SplitApp.Modules.Expenses.Domain (for Currency refs)
Modules/Expenses/SplitApp.Modules.Expenses.Domain
   → Modules/Users/SplitApp.Modules.Users.Domain (for AppUser refs)

This bends the strict "no direct references between modules" rule from phase3.md at the Domain level. Application, Infrastructure, and Api projects remain isolated — they never <ProjectReference> another module — and continue to use MediatR for actual function calls. Schema isolation, MediatR-only inter-module communication, and per-module DbContext ownership are all preserved at runtime; only the entity type system is shared.