Skip to content

Source Code — Core Classes

The classes almost every request passes through, and the pattern the 44 business classes follow. If you read only one page of this section before changing code, read this one.

The request-scoped trio

Three small classes carry the caller's identity from the authorization filter into every controller, view and business class for the rest of the request.

Class File Lifetime Role
CurrentRequestData NKO.Shared/Helpers/Contracts/CurrentRequestData.cs (12 ln) Scoped A plain bag: Employee (AppUser), User (ApplicationUser), Permissions (List<string>). User is never assigned anywhere — it is always null.
PermissionsHandler NKO.Shared/Helpers/Sec/PermissionsHandler.cs (44 ln) Transient Reads the caller's permission keys from the database.
PermissionChecker NKO.Shared/Helpers/Sec/HasPermission.cs (20 ln) Scoped HasPermission("Key") — used in views to show or hide a control.

CurrentRequestData is filled only by ClaimRequirementFilter, and only on the success path — and only two of its three properties: Employee and Permissions. Its User property is never set by any code in the solution. That has a consequence worth internalising:

CurrentRequestData.Employee is null on an ungated action

An action with no [ClaimRequirement] never runs the filter, so nothing populates CurrentRequestData. Any code reading _currentRequestData.Employee there gets null. This is part of why the ungated actions (R3-2, R3-7) matter beyond authorization: several of them would throw rather than leak.

PermissionsHandler.GetUserPermissions(username)

Returns KeyValuePair<AppUser, List<string>> — the employee record and their keys in one call.

  1. Load ApplicationUser by UserName.
  2. Load the AppUser by IdentityUserId, eager-loading AppUserGroupMember, AppUserExtend, CommonBranch, CommonSubBranch.
  3. If !user.IsEnableLogin → return the employee with an empty key list. A disabled login is authenticated but can reach nothing that needs a key.
  4. Otherwise load every AppUserGroupMember where IsEnable, include AppUserGroup → AppUserGroupPermission → AppPermission, and flatten every PermissionName into the list.

This runs on every request to a gated action — there is no cache. Both lookups use FirstAsync, which throws when there is no match, so an ApplicationUser with no matching AppUser row produces an unhandled InvalidOperationException (and, per R1-1, the developer exception page).

The two permission checks disagree on case — R8-3

ClaimRequirementFilter compares case-sensitively:

var hasClaim = userPermissions.Value.Any(c => c == _claim.Value);

PermissionChecker, used by the views, compares case-insensitively:

return _data.Permissions.Any(x => x.ToLower() == permission.ToLower());

A key stored as humanresourceindex against an attribute reading HumanResourceIndex would therefore render the menu item and then 403 the click. Keys are free-form strings typed by an administrator, so the mismatch is reachable through the UI, not just theoretical.

ClaimRequirementFilter

NKO.Shared/Helpers/Sec/ClaimRequirementFilter.cs (60 ln), namespace NKO.Shared.Web.Helpers. Two classes in one file:

  • ClaimRequirementAttribute : TypeFilterAttribute — what you write as [ClaimRequirement("Key")]. It packs the key into a Claim with an empty type and passes it as a filter argument.
  • ClaimRequirementFilter : IAsyncAuthorizationFilter — the logic.
flowchart TD
    A["OnAuthorizationAsync"] --> B{"User.Identity.IsAuthenticated?"}
    B -- no --> C["RedirectResult ~/Account/Login?ReturnUrl=path"]
    B -- yes --> D["PermissionsHandler.GetUserPermissions — hits the DB"]
    D --> E{"key present? (case-sensitive)"}
    E -- no --> F["ForbidResult + StatusCode 403"]
    E -- yes --> G["fill CurrentRequestData.Employee + .Permissions"]
    F --> H["trailing check: status == 403 → redirect ~/Error/UnAuthorised"]
    G --> I["action runs"]

Note the trailing if (context.HttpContext.Response.StatusCode == 403) sits outside the authenticated branch, so it also catches a 403 set earlier in the pipeline.

BUser — the class everything depends on

NKO.Shared/Helpers/BUser.cs, 1 909 lines, namespace NKO.BAL. The largest shared class and the most widely injected. Despite the name and namespace it is not in NKO.Shared/BAL/ — it is in Helpers/.

94 public methods, in six groups:

Group Representative methods
Lookup Get, GetByCode, GetByEmail, GetByName, GetAll, GetActiveAll, GetInactiveUsers, SearchUsers
Org hierarchy GetManager, GetTechnicalManager, GetAllManagers, GetDeptManager, IsManagerOfUser, GetChildDepts, GetDeptTreeAsString, GetMySupervisionDepts
Named role-holders GetHRManager, GetSCManager, GetFinanceManager, GetCountryDirector, GetBranchLogistic, GetBranchFinance, GetBranchHR, GetBranchTreasure, GetProjectsFocalPoints
Contract & salary GetContract(sAsync), GetSalaryContract, GetcurrentSalary, GetNetSalary, GetUserLevel, GetPosition, GetFullUserPosition
Calendar GetWorkingDays (3 overloads), AddWorkdays, IsHoliday, StartOfWeek
Security & audit GetPermission (data scoping), VerifyPin, ChangePin, ResetPinCode, SetCustomTracking, GetFilteredHistory, ExportUserHistory, DisableUsers

GetPermission(SharedPermissions.<Area>, userId) is the one to know: it returns the row-level scope (All / Location / Custom / Default) that callers apply by hand inside their own queries. See Security & Permissions.

StaffPermissionchecker — the org subtree

NKO.Shared/BAL/Permissionchecker.cs (163 ln). Registered Scoped. Walks ManagerId / TechManagerId so a line manager sees their own people and no one else's.

Method Returns
GetStaff(userId) Every employee below this user, recursively
GetDirectStaff(userId) Direct reports only
GetStaffWithoutMe(userId) The subtree excluding the caller
GetStaffObj(userId) The subtree as List<UserStaffVM> for display
GetManagersSequence(userId) The chain upward — used to resolve approvers
IsEmployeeOfMyStaff(employee, manager) Guard used before showing another employee's record

DataPermissionType.Default in the scoping switch resolves through GetStaff, which is what makes "my team only" work across leave, timesheet, overtime and advance screens.

BSign — the e-signature gate

NKO.Shared/BAL/BSign.cs (336 ln). Every workflow transition calls BSign.HasSign(userId) before doing anything; without an approved AppUserSign the transition is refused with "Please upload your E-signature." Uploading a signature is itself an approval workflow, so signing authority is administered rather than self-granted. GetSign(id) returns the stored base64 image, which BaseController.Thumbnail renders.

Identity classes

Class File Notes
ApplicationUserManager Helpers/Sec/ApplicationUserManager.cs (24 ln) Subclasses UserManager<ApplicationUser>; its constructor assigns PasswordHasher = new BCryptPasswordHasher(), overriding the Identity default.
BCryptPasswordHasher Helpers/Sec/BCryptPasswordHasher.cs (54 ln) IPasswordHasher<ApplicationUser> over BCrypt.Net, salt generated at work factor 12.
ClaimsProvider Helpers/Sec/ClaimsProvider.cs (34 ln) A UserClaimsPrincipalFactory<ApplicationUser> that would add every permission key to the sign-in cookie as a Role claim. Dead code — it is not registered in DI and nothing references it, so Identity uses its default factory.

BCryptPasswordHasher is not resolved from the container

Startup.cs registers it twice (services.AddTransient<BCryptPasswordHasher>(), the same line repeated), but nothing injects it — ApplicationUserManager news it up directly. Both registrations are dead. Work factor 12 also means any hand-crafted bootstrap row must use the same factor; see R5-1.

The B* business-class pattern

44 classes, all built the same way. There is no interface for 40 of them and no repository layer — each takes DbContext plus whatever collaborators it needs and queries with LINQ.

public class BTraining
{
    private readonly NKO_DBEntities1 db;
    private readonly BUser _user;
    private readonly NextUserHelper _nextUserHelper;
    private readonly BNotification _notification;
    private readonly EmailManager _emailManager;
    // … ctor assigns them
}
Convention Shape
Class name B<Entity> (BPayroll, BLeave) or <Feature>Handler (AllocationHandler)
Create Create(<Entity> model) — assigns a new Guid, StatusId = Draft, NextUserId = CreatedBy
Visibility GetMy<Things>(userId) applies data scoping; GetAll(...) does not
Transition Set<Role><Outcome> — SetManagerAccepted, SetHRRefused, SetSent
Transition guard NextUserId == currentUser and an exact StatusId and prior sign flags
Transition body set …Sign / …SignDate / …Note / …Id, advance StatusId, resolve NextUserId via NextUserHelper, notify + e-mail

The five largest classes

Class Lines Project folder
BAsset 4 205 WareBAL/
BStock 4 024 WareBAL/
BRFQ 3 843 BAL/
BLeave 2 703 BAL/
BJobs 2 689 BAL/

Together these five are 17 464 lines — about 40 % of all business logic. They hold the most intricate rules in the system (CBA supplier scoring, stock availability and release, leave accrual, interview scoring) and are the ones to approach most carefully.

The five interface-backed services

Only these are registered against a contract; the other 40 business classes are injected as concrete types.

Interface Implementation
IS3ServiceHandler S3ServiceHandler
IProcBudgetLineHandler ProcBudgetLineHandler
IAllocationHandler AllocationHandler
IBudgetLinesHandler BudgetLinesHandler
IAttendancesHandler AttenadancesHandler (note the spelling)

See Services & DI for the full registration list and its defects.