Skip to content

Architecture — Request Pipeline

How a request is hosted, routed and authorised, read directly from Program.cs and Startup.cs.

Hosting

Program.cs uses the generic host with the classic Startup class:

Host.CreateDefaultBuilder(args)
    .ConfigureServices((hostContext, services) =>
    {
        services.AddQuartz(q => { /* ExportHistoryJob, UpdateNextUserJob */ });
        services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
    })
    .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup<Startup>());

Quartz is registered here, before the web host — the other two schedulers are registered inside Startup. See External Services.

Middleware order

Startup.Configure builds this pipeline, in this order:

flowchart TD
    A["Request"] --> B["UseDeveloperExceptionPage - unconditional"]
    B --> C["UseHsts - non development only"]
    C --> D["UseHttpsRedirection"]
    D --> E["UseStaticFiles"]
    E --> F["Rotativa setup"]
    F --> G["Custom middleware - collapse double slash to single"]
    G --> H["UseRouting"]
    H --> I["UseAuthentication"]
    I --> J["UseAuthorization"]
    J --> K["UseRequestLocalization"]
    K --> L["UseEndpoints - SignalR hub at slash hub"]
    L --> M["UseEndpoints - default controller route"]
    M --> N["Controller action"]
Step What it does
UseDeveloperExceptionPage() Detailed error page. Called unconditionally — see the warning below.
UseHsts() Strict-Transport-Security, only when not in Development. The matching if (env.IsDevelopment()) branch is empty.
UseHttpsRedirection() Redirects HTTP to HTTPS
UseStaticFiles() Serves wwwroot/
RotativaConfiguration.Setup(env.WebRootPath, "Rotativa") Points the PDF renderer at wwwroot/Rotativa
Inline middleware Rewrites a path beginning // to / before routing
UseRouting() Matches the endpoint
UseAuthentication() Establishes HttpContext.User
UseAuthorization() Runs authorization filters, including ClaimRequirementFilter
UseRequestLocalization() Resolves the culture — see Localization
UseEndpoints() ×2 First maps the SignalR hub, then the default controller route

Developer exception page runs in production — R1-1

app.UseDeveloperExceptionPage();   // no environment check

if (env.IsDevelopment())
{
}                                  // empty branch
else
{
    app.UseHsts();
}
The call sits outside the environment check and the Development branch is empty, which looks like a line that was moved out and never moved back. In production an unhandled exception therefore returns a full diagnostic page — stack trace, source-code excerpts, routing data, request headers and environment variables.

There is also no UseExceptionHandler anywhere in the pipeline, so there is no friendly fallback error page. Tracked as R1-1 in Recommendations.

UseRequestLocalization is registered after UseRouting, UseAuthentication and UseAuthorization. It still applies before the action executes, but it is later than the usual placement and means authentication and authorization run under the default culture.

Routing

One conventional route, registered last:

endpoints.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
  • No areas. Warehouse controllers live in a different folder but share the same flat namespace, so /Stock/Index and /PurchaseRequest/Index sit side by side.
  • Default landing: / resolves to HomeController.Index.
  • One optional {id} segment. Everything else arrives as query string or form fields.
  • Four attribute-routed exceptions carry [Route("api/…")] — the file endpoints described in the API Overview.

Because there is only one template, a route is fully predictable from the class and method name: PurchaseRequestController.Details is /PurchaseRequest/Details/{id}.

Authorization filter

ClaimRequirementFilter (NKO.Lib/NKO.Shared/Helpers/Sec/ClaimRequirementFilter.cs) implements IAsyncAuthorizationFilter and is attached per action by [ClaimRequirement("Key")]:

flowchart TD
    A["Authorization filter runs"] --> B{"User authenticated?"}
    B -- no --> C["Redirect to tilde slash Account slash Login with ReturnUrl"]
    B -- yes --> D["PermissionsHandler.GetUserPermissions - reads the database"]
    D --> E{"Required key present?"}
    E -- no --> F["ForbidResult, status 403"]
    F --> G["Redirect to tilde slash Error slash UnAuthorised"]
    E -- yes --> H["Populate CurrentRequestData - Employee and Permissions"]
    H --> I["Action executes"]

The permission set is loaded from the database on every request, and SecurityStampValidatorOptions.ValidationInterval is set to TimeSpan.Zero, so a group change or a disabled login takes effect on the user's very next click. The cost is a permission query per request.

Full detail in Security & Permissions.

Response shapes

An action typically has several return paths:

Shape Count Behaviour
View(...) 379 Full page in _Layout (_AppLayout for the public /Apps/* pages)
RedirectToAction(...) / Redirect(...) 303 Post-Redirect-Get carrying msg / errorMsg
PartialView(...) 276 Fragment loaded into a Bootstrap modal
Json(...) 270 AJAX payload, serialised by Newtonsoft with reference loops ignored
File(...) / FileStreamResult 16 Excel / PDF / attachment download

Failures are normally expressed as a redirect carrying a message, not as a status code — an HTTP client cannot distinguish a refused workflow transition from success by status alone.

Localization

Three cultures are configured — en, ar, tr — with en as the default and dd.MM.yyyy as the short date pattern across all of them. Culture is resolved by three providers, in order:

  1. QueryStringRequestCultureProvider — ?culture=ar
  2. CookieRequestCultureProvider — set by /Culture/SetLanguage
  3. AcceptLanguageHeaderRequestCultureProvider — the browser header

Resources live under Resources/ and are resolved against SharedResource; NKO.Shared's views are served from the assembly through EmbeddedFileProvider.

Real-time

services.AddSignalR() plus a hub mapped at /hub (RealTime), with a custom IUserIdProvider (CustomUserIdProvider) so notifications can be addressed to a specific employee. RealTimeNotifier is the BAL-side wrapper that pushes notification counts to the top bar.