Multi-role HR system built with ASP.NET Core MVC (.NET 8), SQL Server, and ADO.NET (no ORM). Three-layer architecture: LeaveSystem.Data β LeaveSystem.Bll β LeaveSystem.Web. Built across feature branches to demonstrate a real git workflow.
Leave management is a daily workflow in every organisation, yet it's surprisingly hard to model correctly: managers approve only their team's requests, teams can span multiple hierarchy levels, and the same employee can be both a Manager and a subordinate. I built this system to tackle recursive organisational hierarchy, concurrency-safe approvals, and SOLID service design β problems I encounter regularly in enterprise backend work.
- Recursive team hierarchy β
sp_GetTeamHierarchy(recursive CTE) resolves multi-level manager chains; managers approve all direct and indirect reports - Concurrency-safe decisions β
sp_DecideLeaveRequestruns withUPDLOCK/HOLDLOCKto prevent two managers simultaneously approving the same request - SOLID refactor β Decomposed a 186-line
Services.csmega-file into 7 single-responsibility service classes (AuthService, LeaveRequestService, EmployeeService, β¦) - 35+ unit tests β LeaveRequestService (balance checks, team-scope authorization), AuthService, Manager service layer (xUnit + Moq)
- Integration tests β
WebApplicationFactoryverifying role-based redirects for Employee/Manager/HR routes - CI/CD β GitHub Actions with test coverage upload
- Containerized β Multi-stage Dockerfile + docker-compose with SQL Server 2022
| Role | Capabilities |
|---|---|
| HR | Edit any employee, see all leave requests, manage department/leave-type catalogs, view dashboard. Can approve/reject any request. |
| Manager | Approve / reject leave requests for direct and indirect reports (recursive hierarchy). View team's monthly calendar. Submit own leave (approved by their own manager / HR). |
| Employee | View profile, leave balance, request new leave (with client + server validation), cancel own pending requests. |
ββββββββββββββββ
β Employee β
β submits β
β request β StartDate, EndDate, Type, Reason
ββββββββ¬ββββββββ
β status: Pending
βΌ
ββββββββββββββββββββ
β sp_GetLeaveBalance βββ enforces quota in BLL before insert
ββββββββ¬ββββββββββββ
β
βΌ
ββββββββββββββββ approve ββββββββββββββββ
β Manager βββββββββββββββββΆβ Approved β β balance reduces
β reviews β ββββββββββββββββ
β (or HR) β reject ββββββββββββββββ
ββββββββββββββββββββββββββββββββΆβ Rejected β β no balance change
ββββββββββββββββ
Employee cancel ββββββββββββββββ
(only Pending)βββββββββββΆβ Cancelled β
ββββββββββββββββ
Each transition is recorded in AuditLog (entity, actor, old β new, timestamp).
LeaveSystem/
βββ Database/
β βββ 001_Schema.sql β tables, indexes, hierarchical FK
β βββ 002_StoredProcedures.sql β sp_GetLeaveBalance, sp_GetTeamHierarchy (CTE), sp_DecideLeaveRequest, sp_GetHrDashboard
β βββ 003_SeedData.sql β 3 roles, 4 departments, 8 employees with hierarchy, sample leave history
βββ src/
βββ LeaveSystem.Data/ β entities + repositories (ADO.NET)
β βββ DbConnectionFactory.cs
β βββ Entities/
β βββ Repositories/
β βββ Interfaces/
βββ LeaveSystem.Bll/ β business logic (validation, authorization, audit)
β βββ Services/Interfaces/
β βββ DTOs/
β βββ Helpers/ β PasswordHasher (HMAC-SHA512)
βββ LeaveSystem.Web/ β MVC web app
βββ Controllers/ β Account, Home, Employees, Leave, Manager, Hr
βββ Views/ β Razor with shared `_Layout`, `_ValidationScriptsPartial`
βββ ViewModels/
βββ wwwroot/css, wwwroot/js
βββ Program.cs
βββ appsettings.json
- Self-referencing
Employees(ManagerId β Employees.Id)β recursive CTE insp_GetTeamHierarchyreturns direct + indirect reports sp_GetLeaveBalance(@EmployeeId, @Year)β annual quota minus approved + pending leave days, with optional per-employee override viaLeaveQuotassp_DecideLeaveRequestβ wraps approve/reject in a transaction, locks the row with(UPDLOCK, HOLDLOCK), writes toAuditLogatomicallysp_GetHrDashboardβ returns 4 result sets in one round-trip (headcount, on-leave-today, usage, top counts)- Critical indexes:
IX_LR_Employee_Range,IX_LR_Status_Start,IX_Audit_Entity
sqlcmd -S "(localdb)\mssqllocaldb" -i Database/001_Schema.sql
sqlcmd -S "(localdb)\mssqllocaldb" -i Database/002_StoredProcedures.sql
sqlcmd -S "(localdb)\mssqllocaldb" -i Database/003_SeedData.sqlSchema script drops & recreates
LeaveDb. All scripts useSET QUOTED_IDENTIFIER ON.
cd src/LeaveSystem.Web
dotnet run| Role | Notes | |
|---|---|---|
hr@leave.test |
HR | Sees everything |
sara@leave.test |
Manager | Engineering manager β Alice, Bob, Carol report to her |
mike@leave.test |
Manager | Sales manager β Dan, Eve report to him |
alice@leave.test |
Employee | Has approved + pending requests |
carol@leave.test |
Employee | Currently on leave |
This repo was built across feature branches; check git log --graph --oneline --all to see the merge history:
* Merge feature/docs
* feat(docs): comprehensive README
* Merge feature/hr-dashboard
* feat(hr-dashboard): department headcount, on-leave-today, leave usage stats
* Merge feature/leave-requests
* feat(leave-requests): submit/cancel/details, manager approvals + team monthly calendar
* Merge feature/web-foundation
* feat(web): foundation β auth, layout, employee list/details/edit, home with leave balance
* Merge feature/data-bll
* feat(data,bll): repositories with raw ADO.NET, services with role-based authorization
* fix(database): correct sp_GetLeaveBalance to use scalar subquery
* Merge feature/database
* feat(database): schema with hierarchical employees, stored procs, seed data
* chore: initial scaffold (3-project solution + .gitignore)
Each feature branch was created from main, committed in isolation, and merged with --no-ff to preserve the merge commit (clearer history).
| Path | Auth | Description |
|---|---|---|
/ |
All | Personal dashboard (employee) or HR redirect |
/Employees |
All | Browse, search, filter by department |
/Employees/Details/{id} |
All | Profile + leave history + balance + audit log |
/Employees/Edit/{id} |
HR | Update profile, change manager / department |
/Leave/Mine |
All | Own leave requests + balance |
/Leave/Create |
All | Submit new leave (live day count, validation) |
/Leave/Details/{id} |
Owner / Manager / HR | Single request view + cancel |
/Manager/Approvals |
Manager / HR | Approve / reject queue (filterable by status) |
/Manager/Calendar |
Manager / HR | Team monthly leave grid |
/Hr |
HR | Headcount, usage, on-leave-today |
- Server-side:
[Required],[EmailAddress],[Compare], custom checks in services (date range, balance, role). - Client-side: jQuery Validation Unobtrusive (built into MVC scaffolding) + custom
submithandler inLeave/Create.cshtmlfor live day count + end-date check.
- HMAC-SHA512 + per-user salt
- Cookie auth, HTTP-only, 14-day sliding expiration
- Anti-forgery tokens on every state-changing form
- All SQL parameterized
- Decision SP runs at lock isolation to prevent double-decisions
- Manager-can-only-decide-own-team enforced in service layer