Skip to content

Repository files navigation

Course Registration System

A course registration and management web application built with ASP.NET Core MVC.
Students browse and apply to courses; administrators manage courses, instructors and applications.

CI


Overview

The system separates two roles:

  • Students register an account, browse the course catalogue, and apply to a course.
  • Administrators sign in to a management area where they create and edit courses, manage the instructor roster, and review incoming applications.

Data is persisted with Entity Framework Core against SQLite, with schema changes tracked through migrations.

What the test suite found

The application worked. Adding tests to it was supposed to be a formality — writing down behaviour that already held. Several of the first tests failed, and the reasons were not cosmetic.

Finding What it meant
AdminController had no [Authorize] Every management action answered anonymous requests. POST /Admin/KursSil deleted a course — and cascaded to its applications — with no session at all.
Administrator credentials were string literals if (kullaniciAdi == "admin" && sifre == "1234") sat in the login action. The working password shipped with the source.
Passwords were stored and compared in clear text o.Sifre == sifre. A read of the database was a read of every account's password.
Cancelling an application never checked ownership The endpoint took an application id and deleted it. Any signed-in student could cancel anyone else's place by incrementing an id.
Capacity was a read-then-write race Applications were counted, compared against Kontenjan, and only then inserted.
Identity was resolved by display name o.AdSoyad == User.Identity.Name. Two students with the same name resolved to whichever row the database returned first.

The capacity bug is the one worth measuring. Reproducing the original logic under 15 concurrent applications to a course with capacity 5:

old logic (count → compare → insert):   15 enrolled     ← 3× over capacity
current logic (conditional UPDATE):       5 enrolled

That measurement is what Es_zamanli_basvurular_kontenjani_asamaz locks in.

How capacity is enforced now

Kurs carries a KayitliSayisi counter, and enrolment claims a seat and checks capacity in a single statement:

UPDATE Kurslar SET KayitliSayisi = KayitliSayisi + 1
WHERE Id = @id AND KayitliSayisi < Kontenjan

Zero rows affected means the course was full. There is no window between the check and the write for a second request to slip through. The insert that follows runs in the same transaction, and a unique index on (KursId, OgrenciId) settles duplicate applications at the database rather than in application code — the application-level check is a nicety for the error message, not the thing being relied on.

Security

Concern Approach
Password storage PBKDF2-HMAC-SHA256, 210 000 iterations, 128-bit random salt per password. The iteration count is embedded in the stored hash so it can be raised without invalidating existing rows. Comparison is fixed-time.
Administrator account A row in Yoneticiler, seeded from configuration. With no password configured, development generates a random one and logs it once; production refuses to create the account.
Authorization [Authorize(Roles = "Admin")] at the class level on the management area, verified by integration tests that drive the real HTTP pipeline.
CSRF AutoValidateAntiforgeryTokenAttribute registered globally, so a new POST action is protected by default rather than when someone remembers the attribute.
Ownership Cancellation compares the application's owner against the caller's NameIdentifier claim.
Open redirect returnUrl is followed only when Url.IsLocalUrl accepts it.
User enumeration Login does not distinguish "no such user" from "wrong password", and runs a hash verification even when the user does not exist so the two paths take comparable time.
Dependencies CI fails the build on any package with a known advisory, including transitive ones.

Architecture

KursKayitSistemi/
├── Controllers/     Thin: authorize, bind, delegate, choose a view
├── Services/        Rules with real invariants — enrolment, accounts, password hashing
├── Models/          Entities + AppDbContext (indexes, delete behaviour, constraints)
├── ViewModels/      Binding targets, kept separate from entities to prevent over-posting
├── Views/           Razor views per controller + shared layout
└── Migrations/      EF Core schema history

Not every controller got a service. Enrolment did, because it has invariants that must hold under concurrency and is worth testing in isolation. Instructor and course CRUD stayed in AdminController: wrapping a single Add and SaveChanges in a service class would add a layer without adding a rule.

Data model

Entity Purpose
Kurs A course, with Kontenjan (capacity) and the maintained KayitliSayisi
Egitmen An instructor. Deleting one is Restrict, not Cascade — courses must be dealt with first
Ogrenci A registered student; stores SifreHash, never a password
Basvuru A student's application. (KursId, OgrenciId) is unique
Yonetici An administrator account

Getting started

Requirements: .NET SDK 10.0+

git clone https://github.com/kutsibalci/Course-Registration-System.git
cd Course-Registration-System

dotnet run --project KursKayitSistemi

Migrations are applied at startup, so there is no separate database update step. On the first run the console prints a generated administrator password:

warn: DatabaseSeeder[0]
      SeedAdmin:Sifre tanimli degil. Gelistirme icin rastgele bir yonetici sifresi uretildi.
          Kullanici adi : admin
          Sifre         : 7Qk2mZ0pXbNc

To choose it yourself instead:

dotnet user-secrets set "SeedAdmin:Sifre" "kendi-sifreniz" --project KursKayitSistemi

Upgrading an existing database: the migration drops the clear-text Sifre column. A hash cannot be recovered from it, so student accounts must be created again.

Tests

dotnet test

62 tests, no external dependencies — each one creates its own SQLite file and runs the real migrations against it. The in-memory provider is deliberately not used: it supports neither transactions nor ExecuteUpdate, which are exactly the mechanisms the capacity fix depends on.

Suite Covers
PasswordHashServiceTests Round-trip, per-password salting, embedded iteration count, malformed stored hashes
AccountServiceTests Registration, duplicate detection, authentication, role assignment
EnrollmentServiceTests Capacity, duplicates, ownership, seat release, 15-way concurrent enrolment
AuthorizationIntegrationTests The real HTTP pipeline: anonymous, student and administrator against every management route; antiforgery; the retired hard-coded password
SchemaTests Model/migration drift, unique indexes, cascade and restrict behaviour

Notes

Built as a learning project to work through the full ASP.NET Core MVC stack. The most useful part was not the first version — it was discovering, by writing tests against something that appeared to work, how much of "working" was untested assumption.


Built by Hüseyin Kutsi Balcı · MIT licensed

About

Course registration and management web app built with ASP.NET Core MVC, EF Core and SQLite - student applications, plus an admin panel for courses and instructors.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages