Skip to content

Architecture — Overview

The NKO ERP is a single ASP.NET Core MVC application (.NET 6) that renders HTML on the server and talks to two SQL Server databases. There is no SPA front end, no separate API tier and no microservices: one deployable web application, one shared business-logic layer, and two Entity Framework contexts.

At a glance

5 projects · 44 controller classes / 947 actions · 44 business-logic classes · 161 entities across 2 databases · 219 permission keys · .NET 6 / EF Core 6.0.15 / SQL Server.

Layering

The solution follows a conventional three-layer shape. Controllers hold almost no business logic; they resolve the current user, call a B* / *Handler class, and hand the result to a view.

flowchart TD
    subgraph P["Presentation"]
        V["Razor Views and ViewComponents"]
        C["Controllers - 44 classes, 947 actions"]
    end
    subgraph B["Business logic - BAL"]
        M["NKO.Main BAL - 32 classes"]
        W["NKO.Main WareBAL - 8 classes"]
        S["NKO.Shared BAL - 7 classes"]
    end
    subgraph D["Data access - EF Core 6"]
        X1["NKO_DBEntities1 - 140 entities"]
        X2["WarehouseDBContext - 21 entities"]
    end
    subgraph DB["SQL Server"]
        DB1[(NKO_DB)]
        DB2[(NKO_DB_Warehouse)]
        DB3[(Hangfire store)]
    end

    V --> C
    C --> M
    C --> W
    C --> S
    M --> X1
    S --> X1
    W --> X2
    M --> X2
    X1 --> DB1
    X2 --> DB2
    B --> DB3
Layer Where Responsibility
Presentation NKO.Main/Views/, NKO.Shared/Views/, Controllers/, WarehouseControllers/ Razor pages, modal partials, JSON endpoints for jQuery. Controllers validate input and dispatch.
Business logic NKO.Main/BAL/, NKO.Main/WareBAL/, NKO.Shared/BAL/ All rules: approval state machines, payroll and leave calculation, budget roll-ups, CBA scoring, permission resolution.
Data access NKO.Lib/NKO.DataAccess/, NKO.Lib/NKO.Warehouse.DataAccess/ EF Core entities, two DbContexts, migrations. No repository abstraction — the BAL uses DbContext directly.

There is no repository or unit-of-work layer: business classes take a DbContext in their constructor and query it with LINQ. Likewise there are no interfaces for most BAL classes — only five are registered against a contract (IS3ServiceHandler, IProcBudgetLineHandler, IAllocationHandler, IBudgetLinesHandler, IAttendancesHandler) — four of them business classes, plus S3ServiceHandler from Configs/. The other 40 business classes are injected as concrete types.

Lazy loading is not enabled — UseLazyLoadingProxies() is never called, so every navigation property a query needs must be .Included explicitly. See Technology Stack.

The two databases

This is the most important structural fact about the system.

Core Warehouse
Context NKO_DBEntities1 WarehouseDBContext
Connection string HrConnection WareConnection
Database NKO_DB NKO_DB_Warehouse
Entities 140 21
Migrations 18 5
Covers HR, payroll, procurement, finance, recruitment, training, administration, identity Stock, assets, maintenance, destruction, waybills

They are separate databases with no foreign keys between them. The warehouse reuses core identity, signatures, notifications and permissions by calling core BAL classes, and receives goods through one explicit hand-off: BGRN.TransfareToWarehouse writes CenterStock / Asset rows into NKO_DB_Warehouse and flags the core GoodReceivedNote. A third connection string, HangfireConnection, points at the Hangfire job store.

How a request flows

flowchart TD
    R["Browser request"] --> MW["Middleware pipeline"]
    MW --> RT["Routing - controller slash action"]
    RT --> F{"ClaimRequirementFilter"}
    F -- "not signed in" --> L["302 to Account slash Login"]
    F -- "key missing" --> U["403 then Error slash UnAuthorised"]
    F -- "allowed" --> A["Controller action"]
    A --> BAL["BAL class"]
    BAL --> EF["EF Core DbContext"]
    EF --> SQL[(SQL Server)]
    BAL --> EXT["S3, SMTP"]
    A --> OUT{"Response"}
    OUT --> PV["View or PartialView"]
    OUT --> JS["JSON for AJAX"]
    OUT --> RD["Redirect with a message"]

Detail lives in Request Pipeline and Security & Permissions.

Cross-cutting concerns

Concern Mechanism
Authentication ASP.NET Core Identity (ApplicationUser) over NKO_DB; cookie-based. A JWT bearer scheme is registered but never takes effect — see Security & Permissions
Authorization Method-level [ClaimRequirement("Key")], 219 keys, re-read from the database on every request
Row-level scoping BUser.GetPermission(SharedPermissions.Area, userId) returning All / Location / Custom / Default, applied inside the BAL
Org hierarchy StaffPermissionchecker walks ManagerId / TechManagerId to limit a manager to their own staff
Approval workflow One generic engine: an integer StatusId plus NextUserId, driven by 23 ContextActions endpoints
E-signature BSign.HasSign(userId) gates procurement, warehouse, leave, advance and overtime transitions — but not payroll, training, jobs, evaluation, PO justification or destruction (R3-9)
Notifications BNotification rows plus SignalR push over the /hub endpoint
Background work Three schedulers — Quartz.NET, Hangfire and a custom IHostedService. See External Services
Localization English, Arabic and Turkish; resource files under Resources/, culture from query string, cookie or Accept-Language
File storage AWS S3 through IS3ServiceHandler; documents are referenced by storage key
Reporting EPPlus for Excel, Rotativa (wkhtmltopdf) for PDF

Design characteristics worth knowing

  • Server-rendered, jQuery-driven. 379 actions return a full page, 276 return a modal fragment and 270 return JSON to in-page scripts. There is no client-side framework.
  • Conventional routing only. One route template, no areas — see Request Pipeline.
  • Permissions are data, not code. Keys are free-form strings stored in AppPermission and attached to groups; there is no fixed role list.
  • Workflow is uniform. Every multi-step document behaves the same way, which makes the system much easier to learn than its size suggests.
  • Business logic is concentrated. BAsset (4 205 lines), BStock (4 024) and BRFQ (3 843) are the three largest classes and hold the most intricate rules.

Known architectural issues

UseDeveloperExceptionPage() is called unconditionally, so production shows full stack traces (R1-1); there is no default-deny authorization policy (R3-2); and several referenced packages are never used (R1-4). See Recommendations.