Maintenance requests for residential buildings. Residents report problems in the building they live in, company admins assign technicians and review the work, technicians carry it out. The platform admin registers property management companies and their buildings.
- .NET 10 / C# 14, ASP.NET Core Minimal APIs, built-in validation (
AddValidation), ProblemDetails (RFC 9457) - EF Core 10 + PostgreSQL 18 (Npgsql), migrations in
src/K.Fixer.Persistence/Migrations - JWT bearer authentication, one authorization policy per role
- OpenAPI (
Microsoft.AspNetCore.OpenApi) + Scalar - xUnit, Testcontainers (PostgreSQL),
FakeTimeProvider - CSharpier, Husky.Net, JetBrains code inspection
src/
K.Fixer.Domain/ aggregates and rules - no EF Core, no ASP.NET Core, no clock
Maintenance/ MaintenanceRequest (aggregate root) + status history, the status machine
Companies/ Buildings/ Users/
K.Fixer.Persistence/ AppDbContext, table mapping, migrations, reference data and demo seed
K.Fixer.Api/ host
Features/<Area>/ one file per operation (vertical slices): request, response, endpoint, handler
Common/ auth, errors, OpenAPI, paging, database start-up
ApiEndpoints.cs every route; ApiTags.cs - every tag with its description
tests/
K.Fixer.Domain.Tests/ unit tests of the rules (every status move against every status)
K.Fixer.Api.Tests/ HTTP tests on a real PostgreSQL, one container per run
New ──assign──► Assigned ──start──► InProgress ──complete──► Completed ──close──► Closed
▲ │
└──────────start──── Reopened ◄──reopen──────┘
New / Assigned / InProgress / Completed / Reopened ──cancel──► Cancelled
Every move goes through a method of MaintenanceRequest, which checks it and records it in the status history.
A move the rules do not allow is 409 Conflict with a stable code (e.g. maintenance-request.invalid-status).
Two people changing the same request at once get one success and one 409 (xmin concurrency token).
- .NET SDK 10 (
global.json) - Docker (tests start PostgreSQL in a container; the showcase stack runs in compose)
dotnet restore # also restores local tools (CSharpier, Husky, dotnet-ef) and installs the git hooksThe whole stack (PostgreSQL, migrations, demo data, API with the web client) - showcase/:
showcase/start.sh # http://localhost:25080/login; -stop, -cleanFrom the IDE / dotnet run (Development, http://localhost:25000): the database comes from the shared local Docker
Swarm hr-local (PostgreSQL on localhost:5432, see src/K.Fixer.Api/appsettings.Development.json):
deploy/deploy.sh --db-only # database kfixer, migrations, demo data
dotnet run --project src/K.Fixer.ApiDemo accounts (password fixer777): admin (platform admin), victoria@cme.com (company admin), anna@cme.com
(technician), daniel@cme.com (resident) - or the one-click buttons on the sign-in page (DemoSignIn:Enabled).
| Setting (environment variable) | Default | |
|---|---|---|
ConnectionStrings__Main |
- | required |
Jwt__Key |
- | required secret, at least 32 characters |
Jwt__Issuer, Jwt__Audience |
KFixer |
|
Jwt__TokenLifetime |
06:00:00 |
5 minutes to 1 day |
DemoSignIn__Enabled |
false (true in Development) |
one-click sign-in as each role, no password; never in production |
ScalarVisible |
false |
OpenAPI document and Scalar outside Development |
K.Fixer.Seed (one-off, after Liquibase): Seed__Showcase (false) adds the demo data to an empty database -
160 companies (some deactivated), each with 1 to 10 buildings, company admins, technicians and residents (with past
stays, some locked or deactivated), two platform admins; Seed__MaintenanceRequests (5000, 0 to 100 000) is the
number of maintenance requests at every stage, a fifth of them in Acme (the demo accounts' company).
Wrong or missing settings stop the API at start-up with the reason.
Health: GET /health/live (process), GET /health/ready (database reachable).
showcase/: the whole stack in one command, no.env-cd showcase && docker compose up --build, then http://localhost:25080 (seeshowcase/README.md).deploy/deploy.sh: on the shared local Docker Swarmhr-local(databasekfixer, rolekfixer_app, containerkfixeronlnet), http://127.0.0.1:6400.--no-demo,--db-only;deploy/remove.sh [--purge].
Both seed the demo data (5000 requests) and enable the demo sign-in buttons.
dotnet build
dotnet test # needs Docker
dotnet csharpier check . # dotnet csharpier format . to fix
scripts/inspect-code.sh # JetBrains inspection (jb), must report nothingGit hooks (Husky.Net): pre-commit - CSharpier check and build; pre-push - build, code inspection and
scripts/web-check.sh (the web client below). HUSKY=0 turns them off (CI, Docker).
web/ - Angular 22 with Clarity, built into the API's wwwroot. Node.js from web/.nvmrc.
cd web
npm start # ng serve on :4200, /api proxied to the API on :25000
npm test # Vitest unit tests, incl. axe-core accessibility rules (jsdom)
npm run check # what pre-push runs: Prettier, production build, tests
npm run a11y # axe-core in Chrome against the running app (showcase/), every role's pages
npm run api:gen # regenerate the API client (orval) after an API changeHow it is written, following angular.dev's best practices for v22:
- Standalone components, zoneless and OnPush by default (v22) - no
standalone,changeDetectionor zone.js. - State in signals (
signal,computed,linkedSignal); data loading withrxResource-core/paged-list.tspages a Clarity datagrid on top of it, going back to page 1 when the filters change. - Forms are Signal Forms (
@angular/forms/signals): a model signal, aschema()with the rules and their messages,[formRoot]+[formField]in the template.core/forms.tssends a form to the API and turns a 400 validation problem into errors on the fields.shared/clarity-form.tsgives Clarity's form layout to a Signal Form (Clarity's ownclrFormexpects the classic forms API). inject(),input()/output()/model(),@if/@for,protectedfor template-only members,@Service().- TypeScript
strict,strictTemplatesandstrictStandalone; the API client is generated, never edited. - Feature folders (
requests/,companies/,buildings/...), shared UI inshared/, cross-cutting code incore/.
scripts/add-migration.sh <Name> # from the model, no running API or database neededRichDomainModel (2026-11) moves data written by the previous version: "no technician" stored as 0 becomes
NULL, the reporter moves from created_by_user_id to reported_by_id, and requests saved without a building
(building_id = 0, a bug in the old resident endpoint) get the building the reporter lived in when they reported
it. If a request still has no known reporter or building, the migration stops and names the count; fix those
rows by hand and start the API again.
The HTTP contract was redone. Clients have to be updated:
- every route is under
/api/v1(/resident-requests→/api/v1/my/maintenance-requests,/api/company/requests→/api/v1/my-company/maintenance-requests,/api/company-manage→/api/v1/companies,/api/technician/requests→/api/v1/my/assigned-requests,/api/auth/session-info→/api/v1/auth/me); - a missing role is
403(was422), an unknown or foreign record is404, a business rule refusal is409, invalid input is400with errors per field - all as ProblemDetails; - creating returns
201withLocationand the created record; state changes return204; - paging is
page/pageSize; ids in responses are the public GUIDs only; - login: wrong user name and wrong password give the same
401.
New: residents can list, read and withdraw their requests; company admins can read a request with its history, close or reopen completed work and list their technicians.