---
title: SAIF.NetCore.ServiceExtensions business rules
summary: Enumerated business rules with per-rule trigger, enforcement, and outcomes.
status: draft
owner: Framework.NCore maintainers
last_reviewed: 2026-10-02
library: SAIF.NetCore.ServiceExtensions
reference_type: package
---

# SAIF.NetCore.ServiceExtensions business rules

This page captures business rules enforced by this library.

## BR-SE-01 Non-secure environment clients cannot retrieve secure settings

| Field | Value |
| --- | --- |
| **Statement** | A non-secure environment client may not retrieve secure settings; no system hash is available in that mode. |
| **Trigger** | `RegisterEnvironmentClient()` registers `BaseEnvironmentClient` and a null `ISystemHashService`. |
| **Enforcement** | `SE/IServiceCollectionExtensions.cs` L51–52; `BaseEnvironmentClient.cs` L744–769. |
| **Outcome** | Secure-setting reads throw `EnvironmentClientException`; `ISystemHashService` resolves to `null`. |
| **Source** | `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/IServiceCollectionExtensions.cs` |

---

## BR-SE-02 A null system hash must not be resolved as required

| Field | Value |
| --- | --- |
| **Statement** | `ISystemHashService` must not be resolved in non-secure mode; the unit test suite asserts a throw. |
| **Trigger** | `GetRequiredService<ISystemHashService>()` call against a non-secure registration. |
| **Enforcement** | `SE/IServiceCollectionExtensions.cs` L52; `SET/StartupTests.cs` L110. |
| **Outcome** | `InvalidOperationException` (MS DI behavior for a factory returning `null` to `GetRequiredService`). |
| **Source** | `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/IServiceCollectionExtensions.cs` |

---

## BR-SE-03 Secure-mode system hash is computed from the caller's assembly

| Field | Value |
| --- | --- |
| **Statement** | In secure mode, the system hash is computed for the assembly of the supplied type (Startup type, console type, or `T` for Windows services). |
| **Trigger** | `RegisterSecureEnvironmentClient(type)`. |
| **Enforcement** | `SE/IServiceCollectionExtensions.cs` L89; `AppStartup.cs` L105; `IHostBuilderExtensions.cs` L31; `IServiceCollectionExtensions.cs` L225. |
| **Outcome** | Hash identity = caller assembly. |
| **Source** | `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/IServiceCollectionExtensions.cs` |

---

## BR-SE-04 DB connection credentials are fetched as a secure, hash-gated setting

| Field | Value |
| --- | --- |
| **Statement** | DB connection-service credentials are fetched as the secure setting `connectionservicecreds`/`dbconnectionclient`, authorized with the WebApp hash of the DBConnectionClient assembly. |
| **Trigger** | First resolve of `IDBConnectionClient`. |
| **Enforcement** | `SE/IServiceCollectionExtensions.cs` L64–73. |
| **Outcome** | Credentials applied to `ConnectionClient`. |
| **Source** | `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/IServiceCollectionExtensions.cs` |

---

## BR-SE-05 The credential value must be a user|password pair

| Field | Value |
| --- | --- |
| **Statement** | The credential value format is `user\|password`. |
| **Trigger** | Splitting the secure setting resolved by [BR-SE-04](#br-se-04-db-connection-credentials-are-fetched-as-a-secure%2C-hash-gated-setting). |
| **Enforcement** | `SE/IServiceCollectionExtensions.cs` L71–72. |
| **Outcome** | Fewer than two parts causes `IndexOutOfRangeException`. |
| **Source** | `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/IServiceCollectionExtensions.cs` |

---

## BR-SE-06 Edge APIs authenticate with JWT bearer only

| Field | Value |
| --- | --- |
| **Statement** | Edge APIs authenticate with JWT bearer only. |
| **Trigger** | `RegisterEdgeAPIAuthServices()`. |
| **Enforcement** | `SE/IServiceCollectionExtensions.cs` L99–110. |
| **Outcome** | Default authentication scheme is JwtBearer. |
| **Source** | `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/IServiceCollectionExtensions.cs` |

---

## BR-SE-07 Core APIs authenticate via a dual JWT/Windows policy scheme

| Field | Value |
| --- | --- |
| **Statement** | Core APIs authenticate via a "dual" policy scheme: an `Authorization` header whose value starts with `"Bearer "` selects JwtBearer; otherwise the Windows/IIS scheme (`IISServerDefaults`, i.e. Negotiate/NTLM via IIS) is selected. |
| **Trigger** | `RegisterCoreAPIAuthServices()`. |
| **Enforcement** | `SE/IServiceCollectionExtensions.cs` L250–259; AuthService `AddDualAuthentication`. |
| **Outcome** | Authentication scheme selected per request. |
| **Source** | `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/IServiceCollectionExtensions.cs` |

---

## BR-SE-08 SecurityApi provides authorization policies dynamically

| Field | Value |
| --- | --- |
| **Statement** | Authorization policies are provided dynamically by `SecurityApiPolicy` and evaluated by `SecurityApiHandler` via `ISecurityClient`. |
| **Trigger** | `AddSecurityApiAuthorizeAttribute()`. |
| **Enforcement** | `SE/IServiceCollectionExtensions.cs` L257; `AppStartup.cs` L129–131. |
| **Outcome** | Policy provider and handler registered for `[SecurityApiAuthorize]`. |
| **Source** | `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/IServiceCollectionExtensions.cs` |

---

## BR-SE-09 Opt-in global authenticated-user requirement

| Field | Value |
| --- | --- |
| **Statement** | Opt-in global rule: every MVC action requires an authenticated user. |
| **Trigger** | `RequireAuthenticatedUser()`. |
| **Enforcement** | `SE/IServiceCollectionExtensions.cs` L116–128. |
| **Outcome** | Global `AuthorizeFilter` added to `MvcOptions`; an unauthenticated request is challenged. |
| **Source** | `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/IServiceCollectionExtensions.cs` |

---

## BR-SE-10 JWT refresh and lower-case URL middleware are conditionally registered

| Field | Value |
| --- | --- |
| **Statement** | JWT refresh service is registered only when `useJwtRefreshMiddleware` is true; `RefreshJwtMiddleware` is added to the pipeline only when `UseJwtRefreshMiddleware` is true; `LowerCaseUrlMiddleware` only when the challenge type is OpenIdConnect. |
| **Trigger** | OIDC registration / pipeline configuration. |
| **Enforcement** | `SE/IServiceCollectionExtensions.cs` L141, L156–159; `AppStartup.cs` L168–176. |
| **Outcome** | Conditional middleware registration/wiring. |
| **Source** | `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/IServiceCollectionExtensions.cs` |

---

## BR-SE-11 Web apps default to Cookie authentication with optional OpenIdConnect challenge

| Field | Value |
| --- | --- |
| **Statement** | Web apps authenticate and sign in with Cookies; the challenge scheme is Cookies by default, or OpenIdConnect if configured. |
| **Trigger** | `AddWebAuthentication(type)`. |
| **Enforcement** | `SE/IServiceCollectionExtensions.cs` L202–209. |
| **Outcome** | Default authentication/sign-in/challenge schemes set accordingly. |
| **Source** | `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/IServiceCollectionExtensions.cs` |

---

## BR-SE-12 Exactly one IErrorHandler must be registered among error handleables

| Field | Value |
| --- | --- |
| **Statement** | Exactly one `IErrorHandler` must be among registered `IErrorHandleable`s; others implementing `IErrorHandlerHook` are treated as hooks. |
| **Trigger** | `DynamicErrorHandler` activation. |
| **Enforcement** | `SE/Middleware/DynamicErrorHandler.cs` constructor. |
| **Outcome** | `InvalidOperationException` if zero or more than one `IErrorHandler` is registered. |
| **Source** | `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Middleware/DynamicErrorHandler.cs` |

---

## BR-SE-13 Exception-to-HTTP-response mapping

| Field | Value |
| --- | --- |
| **Statement** | Exception → HTTP mapping: `HttpStatusCodeException` maps to its own status/content type (handled); `SAIFException` maps to 500 `text/plain` (handled); any other exception maps to 500 (unhandled). |
| **Trigger** | An unhandled exception reaches `DynamicErrorHandler.InvokeAsync`. |
| **Enforcement** | `SE/Middleware/DynamicErrorHandler.cs` L50–80. |
| **Outcome** | Error response written with `Error-ID`/`Server-Name` headers, plus a log entry. |
| **Source** | `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Middleware/DynamicErrorHandler.cs` |

---

## BR-SE-14 Hook failures are aggregated, not suppressed

| Field | Value |
| --- | --- |
| **Statement** | Hook failures do not suppress the original error; they are aggregated with it. |
| **Trigger** | An `IErrorHandlerHook` throws during `DynamicErrorHandler` invocation. |
| **Enforcement** | `SE/Middleware/DynamicErrorHandler.cs`. |
| **Outcome** | `AggregateException` containing the original error plus hook failures is logged. |
| **Source** | `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Middleware/DynamicErrorHandler.cs` |

---

## BR-SE-15 Every request is logged at Debug level

| Field | Value |
| --- | --- |
| **Statement** | Every request is logged at Debug level with web request details. |
| **Trigger** | Any request through a pipeline where `ConfigureDebugRequestHandling()` is applied. |
| **Enforcement** | `SE/Middleware/DebugRequestHandler.cs` L36–46. |
| **Outcome** | A Debug-level log entry is written for every request. |
| **Source** | `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Middleware/DebugRequestHandler.cs` |

---

## BR-SE-16 Legacy cookie gates check presence only and are fail-open

| Field | Value |
| --- | --- |
| **Statement** | Legacy BOL/ASP.NET session gate: a request lacking the relevant cookie is redirected to the `LegacySessionProvider` endpoint with a `returnUrl`; the check is presence-only and the pipeline continues regardless. |
| **Trigger** | A request missing `SAIFAUTH`/`SAIF.BOL.Auth` (`LegacyBolAuthMiddleware`) or `ASP.NET_SessionId` (`AspNetSessionIdMiddleware`). |
| **Enforcement** | `SE/Middleware/LegacyBolAuthMiddleware.cs` L36–47; `SE/Middleware/AspNetSessionIdMiddleware.cs` L35–47. |
| **Outcome** | A 302 redirect is issued, but downstream middleware and the endpoint still execute (fail-open*). |
| **Source** | `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Middleware/LegacyBolAuthMiddleware.cs` |

---

## BR-SE-17 Session support and its gate are opt-in

| Field | Value |
| --- | --- |
| **Statement** | Session support (and the session gate) is enabled only by `EnableSessionMiddleware`. |
| **Trigger** | `AppStartup` constructor sets `EnableSessionMiddleware = true`. |
| **Enforcement** | `SE/Startup/AppStartup.cs` L107–111, L152–156. |
| **Outcome** | `AddSession()` and `AspNetSessionIdMiddleware` are registered/wired only when the flag is set. |
| **Source** | `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Startup/AppStartup.cs` |

---

## BR-SE-18 Web-app auth stack is selected by ASPNETCORE_HOST_LOCATION

| Field | Value |
| --- | --- |
| **Statement** | The web-app auth stack is chosen by `ASPNETCORE_HOST_LOCATION`: `EXTERNAL` selects OIDC; any other value selects Core dual authentication. |
| **Trigger** | `AppStartup.ConfigureServices`. |
| **Enforcement** | `SE/Startup/AppStartup.cs` L62–75, L125–135. |
| **Outcome** | Auth stack selection (OIDC vs. Core dual auth) for the resulting application. |
| **Source** | `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Startup/AppStartup.cs` |

---

## BR-SE-19 HSTS is always on; HTTPS redirect is configurable for web apps

| Field | Value |
| --- | --- |
| **Statement** | HSTS is always on; HTTPS redirection is on by default for web apps unless the machine env var `ASPNETCORE_REDIRECT_SSL` is set to a false value. APIs always redirect. |
| **Trigger** | Pipeline `Configure` execution. |
| **Enforcement** | `SE/Startup/AppStartup.cs` L85–95, L178–181; `CoreAPIStartup.cs` L84–85; `EdgeAPIStartup.cs` L53–54. |
| **Outcome** | `UseHsts()` always runs; `UseHttpsRedirection()` for web apps is conditional on `AppStartup.EnforceSslRedirect`. |
| **Source** | `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Startup/AppStartup.cs` |

---

## BR-SE-20 Console configuration source precedence

| Field | Value |
| --- | --- |
| **Statement** | Console configuration precedence: `appsettings.json` is overridden by environment variables, which are overridden by command line. |
| **Trigger** | `BuildConsoleApp`. |
| **Enforcement** | `SE/IHostBuilderExtensions.cs` L44–60. |
| **Outcome** | Later-added configuration sources win for the console host. |
| **Source** | `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/IHostBuilderExtensions.cs` |

---

## BR-SE-21 Console process exit code reflects ConsoleBase.ExitCode

| Field | Value |
| --- | --- |
| **Statement** | The console process exit code equals `ConsoleBase.ExitCode` after `Finish` runs. |
| **Trigger** | `ConsoleBase.Run()`. |
| **Enforcement** | `SE/ConsoleBase.cs` L28–37. |
| **Outcome** | `Environment.ExitCode` is set from `ConsoleBase.ExitCode`. |
| **Source** | `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/ConsoleBase.cs` |

---

## BR-SE-22 Enabling OpenTelemetry replaces existing logging providers

| Field | Value |
| --- | --- |
| **Statement** | Enabling OpenTelemetry replaces all existing logging providers with OpenTelemetry's own. |
| **Trigger** | `ConfigureOpenTelemetry`. |
| **Enforcement** | `SE/OpenTelemetryExtensions.cs`. |
| **Outcome** | Console and other previously configured logging providers are lost unless re-added*. |
| **Source** | `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/OpenTelemetryExtensions.cs` |

---

## BR-SE-23 All meters and activity sources are collected; all traces sampled

| Field | Value |
| --- | --- |
| **Statement** | All meters and activity sources are collected; all traces are sampled. |
| **Trigger** | `ConfigureOpenTelemetry`. |
| **Enforcement** | `SE/OpenTelemetryExtensions.cs`. |
| **Outcome** | `AddMeter("*")`, `AddSource("*")`, and `AlwaysOnSampler` are applied. |
| **Source** | `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/OpenTelemetryExtensions.cs` |

---

## BR-SE-24 OTLP export requires the OTLP endpoint variable

| Field | Value |
| --- | --- |
| **Statement** | OTLP export is enabled only when `OTEL_EXPORTER_OTLP_ENDPOINT` is set. |
| **Trigger** | `ConfigureOpenTelemetry`. |
| **Enforcement** | `SE/OpenTelemetryExtensions.cs` L62, L108. |
| **Outcome** | No endpoint value means no OTLP exporter is registered. |
| **Source** | `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/OpenTelemetryExtensions.cs` |

---

## BR-SE-25 Required configuration keys must exist

| Field | Value |
| --- | --- |
| **Statement** | Required configuration keys must exist; a missing key throws `ArgumentNullException`. |
| **Trigger** | `GetRequiredValue<T>` / `KeyMustExist`. |
| **Enforcement** | `SE/IConfigurationExtensions.cs` L22, L83–110. |
| **Outcome** | `ArgumentNullException` thrown for a missing or whitespace-only key. |
| **Source** | `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/IConfigurationExtensions.cs` |

---

## BR-SE-26 Controller helpers convert to HTTP errors by throwing

| Field | Value |
| --- | --- |
| **Statement** | Controller helpers convert error conditions to HTTP errors by throwing, relying on `DynamicErrorHandler` to translate the exception into a response. |
| **Trigger** | A controller calls one of the `Throw*` helpers. |
| **Enforcement** | `SE/ControllerBaseExtensions.cs`. |
| **Outcome** | `HttpStatusCodeException` is thrown and handled per [BR-SE-13](#br-se-13-exception-to-http-response-mapping). |
| **Source** | `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/ControllerBaseExtensions.cs` |

---

## BR-SE-27 Username and auth time are derived from JWT claims

| Field | Value |
| --- | --- |
| **Statement** | Username is the upper-cased local part (before `@`) of the first claim whose type contains "nameidentifier"; auth time is parsed from the `auth_time` claim. |
| **Trigger** | A controller derived from `JwtControllerBase` calls `GetUsernameFromClaim()` / `GetAuthTimeFromClaim()`. |
| **Enforcement** | `SE/JwtControllerBase.cs` L22. |
| **Outcome** | An exception during username parsing yields `"Unknown"`; a missing claim yields `null`. If the `auth_time` claim's value fails `long.TryParse` (missing, non-numeric, or absent claim), the method returns `default(DateTime)` (`DateTime.MinValue`) without throwing. If `auth_time` parses as a `long` but that value is outside the range `DateTimeOffset.FromUnixTimeSeconds` supports, the conversion call itself throws uncaught (`ArgumentOutOfRangeException`) — this is a distinct failure mode from the parse-failure default, not also absorbed into `DateTime.MinValue`. |
| **Source** | `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/JwtControllerBase.cs` |

---

## Notes
- [BR-SE-16](#br-se-16-legacy-cookie-gates-check-presence-only-and-are-fail-open)'s "fail-open" characterization is inferred from the unconditional `await next(context)` call that follows the redirect in both cookie-gate middlewares; see [security-authorization.md](security-authorization.md) for the full analysis of this control weakness.

## Related documentation
- [Index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Dependencies](dependencies.md)
- [Configuration](configuration.md)
- [Operations and processes](operations-processes.md)
- [Security and authorization](security-authorization.md)
- [Service registration](service-registration.md)
- [API reference](api/index.md)

> **Note:** Statements marked with `*` were inferred from code structure, naming
> conventions, or contextual evidence where direct confirmation was not available
> in source artifacts. Statements marked with `**` could not be confirmed; treat
> as unknown until verified.

## Sources
- `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/IServiceCollectionExtensions.cs`
- `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Middleware/DynamicErrorHandler.cs`, `DebugRequestHandler.cs`, `LegacyBolAuthMiddleware.cs`, `AspNetSessionIdMiddleware.cs`
- `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Startup/AppStartup.cs`, `CoreAPIStartup.cs`, `EdgeAPIStartup.cs`
- `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/IHostBuilderExtensions.cs`, `ConsoleBase.cs`
- `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/OpenTelemetryExtensions.cs`
- `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/IConfigurationExtensions.cs`
- `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/ControllerBaseExtensions.cs`
- `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/JwtControllerBase.cs`
- `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtenstions.Tests/StartupTests.cs`
