A REST API for a college student portal: authentication and student profiles, course registration, grades and transcripts, and tuition billing. Built with ASP.NET Core and EF Core to demonstrate production-grade .NET backend patterns — not just CRUD, but a real academic domain with rules that matter (registration windows, seat capacity, GPA computation, invoice generation).
Once running, the full API is browsable at /api/v1/docs.
- Auth and student profiles — registration, login, JWT-based auth,
Student/Adminroles, profile CRUD. - Enrollment and course registration — courses, per-term course offerings, enroll/drop with capacity and registration-window checks.
- Grades and transcripts — post grades per enrollment, compute GPA, generate a transcript view.
- Billing and tuition — generate an invoice from a student's enrolled units, record payments, track balance.
Lean, single-project layout: Controllers → Services → AppDbContext, with DTOs at the boundary.
No CQRS/MediatR — a clean layered structure is enough to demonstrate the pattern without over-engineering
for an MVP; it could scale to vertical slices later.
StudentPortal.Api/
Auth/ Identity + JWT issuance, register/login/me, roles
Students/ student profile CRUD
Courses/ courses, terms, offerings (+ remaining-seat counts)
Enrollments/ enroll/drop — window + capacity + duplicate rules live here
Grades/ grade posting, GPA computation (GpaCalculator), transcript
Billing/ invoice generation (InvoiceCalculator), payments, status derivation
Data/ AppDbContext, EF entities, IEntityTypeConfiguration classes, migrations, DbSeeder
Common/ error envelope, exception-handling middleware, paging
Validators/ FluentValidation validators
StudentPortal.Tests/
Unit/ pure logic — GPA math, invoice totals, payment-status transitions (no DB)
Integration/ WebApplicationFactory + Testcontainers Postgres — full HTTP flows
Auth flow: POST /auth/register creates a Student-role account → POST /auth/login returns a
JWT bearer token (HMAC-SHA256, 60 min expiry) → every other endpoint requires
Authorization: Bearer <token>. Admin-only endpoints additionally require the Admin role
(seeded automatically on first run — see below).
Concurrency-safe enrollment: POST /enrollments opens a transaction, takes a SELECT ... FOR UPDATE
row lock on the CourseOfferings row, then checks the registration window, duplicate enrollment, and seat
capacity before inserting — so two students racing for the last seat can't both succeed.
Uses the Philippine college convention: grades run 1.00 (highest) to 5.00 (failing).
GPA is a weighted average of posted grades, weighted by course units — not inverted, so a lower number is a better GPA (consistent with the scale itself):
GPA = sum(grade.Value × course.Units) / sum(course.Units)
computed both per-term and cumulative, over Completed enrollments with a Posted grade. Only
posting a grade marks the enrollment Completed; that's what makes it eligible for the transcript.
See Grades/GpaCalculator.cs.
- Enrollment window — enroll/drop rejected with
409whenTerm.IsRegistrationOpenisfalse. - Capacity —
409when active enrollments already equal the offering'sCapacity; concurrency-safe via row lock (see above). - No duplicate enrollment — unique DB constraint on
(StudentProfileId, CourseOfferingId), checked gracefully in the service first. - Invoice generation —
total = sum(enrolled units in term) × per-unit rate + misc fees; idempotent — regenerating for the same student+term updates the existing invoice rather than duplicating it. - Payment status — derived from
sum(payments)vstotal(Unpaid/Partial/Paid); a payment that would push the total over the invoice amount is rejected with400.
.NET 10 · ASP.NET Core Web API · EF Core 10 (Npgsql) · PostgreSQL · ASP.NET Core Identity + JWT · FluentValidation · Swashbuckle · xUnit + FluentAssertions + Testcontainers.
docker compose up -d # starts Postgres on localhost:5433 (5432 is often already taken on dev machines)
cd StudentPortal.Api
dotnet ef database update # applies migrations
dotnet run # migrates again (idempotent) + seeds + starts the APIThen open http://localhost:5080/api/v1/docs (or whatever port dotnet run prints).
The connection string, JWT signing key, and billing rates live in
appsettings.json. The JWT key is a placeholder — replace it with a real secret (env var / user-secrets) before deploying anywhere that matters.
On first startup (skipped once any Program row exists), Data/DbSeeder.cs creates a realistic,
traceable demo roster — every id is a fixed GUID rather than random, so the same records exist on
every fresh database and can be referenced reliably (e.g. in the .http walkthrough or a demo script):
- An Admin account —
admin@studentportal.local/Admin!12345(override viaSeed:AdminEmail/Seed:AdminPassword) - 4 programs (
BSCS,BSIT,BSBA,BSA) across Computing and Business - 10 courses spanning gen-ed, CS/IT, and business/accountancy
- 2 terms:
2nd Sem 2025-2026(closed — a completed past term) and1st Sem 2026-2027(open — the current term), with 8 course offerings across them - 6 students with realistic names, spread across all 4 programs and different year levels — every student account logs in with
Student!123(e.g.juan.delacruz@studentportal.local/Student!123) - Past-term history: every student completed the same 3 gen-ed/major courses with posted grades (varied 1.00–3.00 range) and a fully Paid invoice — so
GET /transcript/meand a closed billing record are demoable immediately - Current-term activity: students are actively
Enrolledin 2-3 offerings each (no grades yet), with invoices cycling throughUnpaid/Partial/Paidacross the roster so all three billing states are visible without any manual setup. TheBA101offering is seeded at exactly 2/2 capacity to demo a fully-booked offering out of the box.
So the API is demoable immediately after dotnet run — no manual setup required.
dotnet testUnit tests run with no external dependencies. Integration tests spin up a real Postgres container via
Testcontainers (Docker must be running) and exercise the full HTTP stack through WebApplicationFactory —
deliberately not an in-memory provider, since that hides real SQL/constraint behavior.
See StudentPortal.Api/requests.http for a runnable register → login → enroll → grade → transcript →
invoice → payment walkthrough (works with the VS Code REST Client extension or Rider's HTTP client).
- Prerequisite enforcement, faculty role, refresh tokens, rate limiting, and a frontend are explicitly out of scope — see the stretch goals in the original spec.
- The enrollment unique constraint is on
(StudentProfileId, CourseOfferingId)with no status exception, so a dropped enrollment cannot currently be re-created for the same offering.