Skip to content

YahiaHelal/LabAPI

Repository files navigation

Lab API

A laboratory management REST API built with .NET and MS SQL Server, following Clean Architecture principles.


Demo

Lab API Demo

A full walkthrough of all features — authentication, reports, filtering, pagination, localization, and PDF generation.


Table of Contents


Architecture

The solution follows Clean Architecture with a strict dependency rule — dependencies only point inward, never outward.

LabAPI.API  ──►  LabAPI.Application  ──►  LabAPI.Domain
     │                                         ▲
     └──────►  LabAPI.Infrastructure  ─────────┘
                        │
                   LabAPI.Shared
Project Responsibility
LabAPI.Domain Entities, repository interfaces, service interfaces. No external dependencies.
LabAPI.Application Use cases (commands & queries), IUnitOfWork. References Domain only.
LabAPI.Infrastructure EF Core, repository implementations, password hasher, PDF generator.
LabAPI.API Controllers, middleware, Swagger, DI wiring. Composition root.
LabAPI.Shared Result<T>, Error, ReportFilters. Referenced by all layers.

Tech Stack

  • .NET 10 — Web API
  • MS SQL Server — Database
  • EF Core — ORM
  • QuestPDF — PDF generation
  • Swashbuckle — Swagger / OpenAPI documentation
  • PBKDF2 (RFC 2898) — Password hashing (built-in)

Project Structure

LabAPI.Domain/
  Entities/           — User, Patient, Session, UserTranslation,
                        RoleTranslation, Report,
                        ReportTypeTranslation, ReportStatusTranslation
  Repositories/       — All repository interfaces (IUserRepository, etc.)
  Services/           — IPdfGenerator, IPasswordHasher

LabAPI.Application/
  Auth/
    Register/         — RegisterCmd, RegisterCmdHandler
    Login/            — LoginCmd, LoginCmdHandler
    Logout/           — LogoutCmd, LogoutCmdHandler
    ChangePassword/   — ChangePasswordCmd, ChangePasswordCmdHandler
    Common/           — AuthResponse
  Users/
    GetProfile/       — GetProfileQuery, GetProfileQueryHandler, ProfileResponse
  Reports/
    Create/           — CreateReportCmd, CreateReportCmdHandler
    Update/           — UpdateReportCmd, UpdateReportCmdHandler
    Pdf/              — GetReportPdfQuery, GetReportPdfQueryHandler
    Queries/
      GetReport/      — GetReportQuery, GetReportQueryHandler, ReportResponse
      GetReports/     — GetReportsQuery, GetReportsQueryHandler, ReportSummaryResponse

LabAPI.Infrastructure/
  Persistence/
    Configurations/   — EF IEntityTypeConfiguration<T> for every entity
    Repositories/     — Concrete repository implementations
    Generated/        — Scaffolded EF entities (reference only, not used directly)
    LabDbContext.cs
    UnitOfWork.cs
  Services/
    PasswordHasher.cs
    PdfGenerator.cs

LabAPI.API/
  Controllers/        — AuthController, ReportsController
  Middleware/         — SessionMiddleware
  Attributes/         — AuthenticatedAttribute, RequireRoleAttribute
  Swagger/            — AcceptLanguageHeaderFilter

Getting Started

Prerequisites

  • .NET 10 SDK
  • MS SQL Server (local or remote)
  • SSMS or any SQL client (to run the DB script)

1. Create the Database

Run the full database script against your SQL Server instance. The script creates all tables, indexes, foreign keys, and seeds lookup data (roles, report types, statuses and their translations).

The project is Database First — EF Core maps to the existing schema. There are no EF migrations.

2. Seed a Staff Account

Since only Staff can register patients, you need at least one Staff account seeded directly.

Step 1 — Generate a password hash by running this locally:

var hasher = new LabAPI.Infrastructure.Services.PasswordHasher();
var (hash, salt) = hasher.Hash("YourPassword");
Console.WriteLine($"Hash: {hash}");
Console.WriteLine($"Salt: {salt}");

Step 2 — Insert the Staff user:

DECLARE @Hash NVARCHAR(255) = N'<paste hash>';
DECLARE @Salt NVARCHAR(255) = N'<paste salt>';

INSERT INTO dbo.Users (Email, RoleCode, PasswordHash, PasswordSalt)
VALUES (N'staff@lab.com', 'STAFF', @Hash, @Salt);

INSERT INTO dbo.UserTranslations (UserId, Locale, DisplayName) VALUES
    (SCOPE_IDENTITY(), 'en', N'Lab Staff'),
    (SCOPE_IDENTITY(), 'ar', N'موظف المعمل');

Important: Always use the N prefix before Arabic string literals in SSMS — otherwise Arabic characters are stored as ????.

3. Configure the Connection String

Edit LabAPI.API/appsettings.json:

{
  "ConnectionStrings": {
    "LabDb": "Server=YOUR_SERVER;Database=LabDb;Trusted_Connection=True;TrustServerCertificate=True;"
  }
}

4. Run the API

cd LabAPI.API
dotnet run

Swagger UI will be available at:

https://localhost:{PORT}/

Authentication

The API uses session-based authentication with HTTP-only cookies.

How it works

  1. POST /api/auth/login — validates credentials, creates a session, sets an HTTP-only cookie (sid) in the response
  2. Every subsequent request — the browser sends the cookie automatically
  3. SessionMiddleware reads the cookie, validates the session, and loads CurrentUser into HttpContext.Items
  4. Endpoints decorated with [Authenticated] reject requests where no valid session is found
  5. Endpoints decorated with [RequireRole("STAFF")] reject non-staff users with 403 Forbidden
  6. POST /api/auth/logout — revokes the session in the DB and clears the cookie

Test Accounts

Staff Account

Field Value
Email staff@lab.com
Password (seeded by you — see Getting Started)
Role STAFF

Patient Account (pre-seeded)

Field Value
Email ahmed@lab.com
Password password
Role PATIENT
Patient No 000002-3

API Reference

Auth

Method Endpoint Auth Description
POST /api/auth/register Staff only Register a new patient, returns generated PatientNo
POST /api/auth/login Public Login, sets session cookie
POST /api/auth/logout Authenticated Revoke current session, clear cookie
PUT /api/auth/change-password Authenticated Change password, revokes all active sessions
GET /api/auth/profile Authenticated Get current user profile (localized)

Reports

Method Endpoint Auth Description
POST /api/reports Staff only Create a new report
GET /api/reports Authenticated List reports with filters and pagination
GET /api/reports/{accNo} Authenticated Get single report (flips IsSeen for patients)
PUT /api/reports/{accNo} Staff only Update report
GET /api/reports/{accNo}/pdf?lang=en Authenticated View PDF inline in browser
GET /api/reports/{accNo}/pdf/download?lang=en Authenticated Download PDF as file

Role restriction on reports: Patients can only see their own reports. Staff can see all reports.


Filtering & Pagination

Filters (query parameters on GET /api/reports)

Parameter Type Description
PatientName string Partial match on localized display name
PatientNo string Exact match
VisitDateFrom datetime Range start
VisitDateTo datetime Range end
AccNoFrom long Range start
AccNoTo long Range end
ReferenceNo string Exact match
ResultDateFrom datetime Range start
ResultDateTo datetime Range end
MobileNo string Exact match
IsSeen bool true or false
StatusCode string PENDING or READY

Pagination (query parameters)

Parameter Default Description
page 1 Page number
pageSize 20 Items per page

Pagination Response Headers

Header Description
X-Total-Count Total number of matching records
X-Total-Pages Total number of pages
X-Page Current page
X-Page-Size Current page size

Localization

The API supports English (en) and Arabic (ar) via a lang query parameter.

GET /api/reports?lang=ar
GET /api/auth/profile?lang=ar
GET /api/reports/220001/pdf?lang=ar

Localized fields:

  • DisplayName — patient name in profile and reports
  • RoleLabel — in profile response
  • ReportType — in report responses
  • Status — in report responses

If lang is missing or unsupported, the API defaults to en.

The lang query parameter approach was chosen over the Accept-Language header because browsers override that header with their own locale when navigating directly to a URL (like opening a PDF in a browser tab), making the header unreliable for PDF endpoints.


Security

Concern Implementation
Password storage PBKDF2 with SHA-256, 350,000 iterations, random 32-byte salt per user
Session token 64-character random hex string (two Guid.NewGuid().ToString("N"))
XSS protection Session token stored in HttpOnly cookie — inaccessible to JavaScript
HTTPS enforcement Cookie marked Secure — only sent over HTTPS
CSRF protection Cookie marked SameSite=Strict — not sent on cross-site requests
Session expiry Sessions expire after 24 hours (ExpiresAt enforced server-side)
Logout Sets RevokedAt on the session in DB and deletes the cookie
Password change Revokes all active sessions for the user, forces re-login
Credential enumeration Login returns the same error for wrong email or wrong password
Role authorization [RequireRole] attribute enforces role checks at the action level

Some Design Decisions

Why lang query parameter over Accept-Language header?

Browsers automatically set the Accept-Language header to their own locale (e.g. en-US) when navigating directly to a URL, overriding any intended locale. This caused PDF view to always render in English regardless of the requested language. The lang query parameter gives explicit, reliable control from both API clients and direct browser navigation.

N+1 query prevention

The GET /api/reports endpoint uses batch loading — all user translations, report type translations, and status translations for a page are fetched in single WHERE IN (...) queries, then resolved via dictionary lookup. Total DB calls: 5, regardless of page size.

Clean Architecture

Domain has zero external dependencies — no EF, HTTP, third-party packages. Infrastructure implements all contracts defined in Domain. The API layer is the composition root and the only place that references Infrastructure directly.


License

MIT License

Copyright (c) 2026 Lab API

About

laboratory management REST API built with .NET and MS SQL Server

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages