---
title: SAIF.Framework.Web.Mvc business rules
summary: Enumerated business rules with per-rule trigger, enforcement, and outcomes.
status: draft
owner: SAIF Documentation
last_reviewed: 2026-09-17
library: SAIF.Framework.Web.Mvc
reference_type: package
---

# SAIF.Framework.Web.Mvc business rules

This page captures business rules enforced by this library. Rules with `Origin: delegated` are enforced by a dependency this library invokes rather than by code in this library.

## BR-01 Guarded arguments reject null or empty input

| Field | Value |
| --- | --- |
| **Statement** | Public entry points must reject null or empty required arguments and null injected services. |
| **Trigger** | Any call into a guarded public method |
| **Enforcement** | `Enforce.ArgumentNotNull` / `Enforce.ArgumentNotNullOrEmpty` calls across the controller, action filter, extension, and service classes in this library |
| **Outcome** | Argument exception thrown before any work is done |
| **Origin** | delegated |
| **Source** | `src/Web.Mvc/Controllers/WebContextControllerBase.cs` |

---

## BR-02 TempData context id takes precedence

| Field | Value |
| --- | --- |
| **Statement** | A web context id carried in TempData takes precedence over any other source. |
| **Trigger** | `OnActionExecuting` |
| **Enforcement** | `WebContextControllerBase.OnActionExecuting`, `TempDataExtensions.ContainsWebContextId` / `WebContextId` |
| **Outcome** | Context id read from `TempData["WebContextId"]` |
| **Origin** | code |
| **Source** | `src/Web.Mvc/Controllers/WebContextControllerBase.cs` |

---

## BR-03 Form field wins over query string for context id

| Field | Value |
| --- | --- |
| **Statement** | When TempData holds no context id, the `WebContextId` form field wins over the `cid` query-string value. |
| **Trigger** | `OnActionExecuting` |
| **Enforcement** | `HttpContextExtensions.GetWebContextId` |
| **Outcome** | Context id resolved from the request, or empty string |
| **Origin** | code |
| **Source** | `src/Web.Mvc/Extensions/HttpContextExtensions.cs` |

---

## BR-04 Cross-application arrival inherits the referring context

| Field | Value |
| --- | --- |
| **Statement** | A request arriving from a different application inherits that application's context for the same session. |
| **Trigger** | `OnActionExecuting` with no explicit context id |
| **Enforcement** | `WebContextControllerBase.OnActionExecuting` |
| **Outcome** | `IWebContextService.Get(applicationName, sessionId)` is called |
| **Origin** | code |
| **Source** | `src/Web.Mvc/Controllers/WebContextControllerBase.cs` |

---

## BR-05 Request-supplied context value always overrides

| Field | Value |
| --- | --- |
| **Statement** | A `WebContextValue` supplied on the request always overrides the stored current value. |
| **Trigger** | `OnActionExecuting`, after context resolution |
| **Enforcement** | `WebContextControllerBase.OnActionExecuting` |
| **Outcome** | `WebContext.CurrentValue` replaced |
| **Origin** | code |
| **Source** | `src/Web.Mvc/Controllers/WebContextControllerBase.cs` |

---

## BR-06 Context and menu decoration require the matching base view model

| Field | Value |
| --- | --- |
| **Statement** | Context and menu decoration apply only to models deriving from the corresponding base view model. |
| **Trigger** | `OnActionExecuted` |
| **Enforcement** | `WebContextControllerBase.OnActionExecuted`, `MenuControllerBase.OnActionExecuted` |
| **Outcome** | Non-conforming models are silently skipped |
| **Origin** | code |
| **Source** | `src/Web.Mvc/Controllers/WebContextControllerBase.cs` |

---

## BR-07 Navigation target and source application derivation

| Field | Value |
| --- | --- |
| **Statement** | The navigation target application is the second segment of the target URL; the source application comes from the referrer. |
| **Trigger** | `Navigate` POST |
| **Enforcement** | `WebContextControllerBase.Navigate` |
| **Outcome** | `sourceApp` / `targetApp` computed |
| **Origin** | code |
| **Source** | `src/Web.Mvc/Controllers/WebContextControllerBase.cs` |

---

## BR-08 Cross-application navigation hands context over via the remote service

| Field | Value |
| --- | --- |
| **Statement** | Cross-application navigation hands the context over through the remote service and clears TempData; same-application navigation hands it over through TempData. |
| **Trigger** | `Navigate` POST |
| **Enforcement** | `WebContextControllerBase.Navigate` |
| **Outcome** | `webContextService.Save(sourceApp, Session.SessionID, WebContext)` + `TempData.Clear()`, or `WebContext.SaveToTempData(TempData)` |
| **Origin** | code |
| **Source** | `src/Web.Mvc/Controllers/WebContextControllerBase.cs` |

---

## BR-09 Every resolved context is cached by id

| Field | Value |
| --- | --- |
| **Statement** | Every context obtained from the remote service is cached in the session repository under its context id. |
| **Trigger** | `CreateContext` / `Get(app, session)` |
| **Enforcement** | `WebContextService` |
| **Outcome** | `items.UpdateOrAdd(context.ContextId, context)` |
| **Origin** | code |
| **Source** | `src/Web/WebContextService.cs` |

---

## BR-10 Context lookup is session-cache first

| Field | Value |
| --- | --- |
| **Statement** | A context requested by id is served from the session cache when present; a cache miss recreates it remotely rather than failing. |
| **Trigger** | `Get(contextId)` |
| **Enforcement** | `WebContextService.Get(string)` |
| **Outcome** | Cached instance returned, or `CreateContext(contextId)` |
| **Origin** | code |
| **Source** | `src/Web/WebContextService.cs` |

---

## BR-11 Exception handling requires the log service and filter context

| Field | Value |
| --- | --- |
| **Statement** | Exception handling is skipped entirely when the log service or filter context is unavailable. |
| **Trigger** | Unhandled action exception |
| **Enforcement** | `HandleExceptionAttribute.OnException` |
| **Outcome** | No logging and no redirect |
| **Origin** | code |
| **Source** | `src/Web.Mvc/ActionFilters/HandleExceptionAttribute.cs` |

---

## BR-12 Logged exception state includes full context and request diagnostics

| Field | Value |
| --- | --- |
| **Statement** | Logged exception state must include the full web context and the request diagnostics. |
| **Trigger** | Unhandled action exception |
| **Enforcement** | `HandleExceptionAttribute.BuildStateInformation`, `HttpContextExtensions.GetValuesAsDictionary`, `WebContextControllerBase.WebContextAsString` |
| **Outcome** | Multi-line state string passed to `LogService.LogException` |
| **Origin** | code |
| **Source** | `src/Web.Mvc/ActionFilters/HandleExceptionAttribute.cs` |

---

## BR-13 Logging failures never surface to the user

| Field | Value |
| --- | --- |
| **Statement** | A failure while logging must never surface to the user. |
| **Trigger** | Logging failure inside the exception filter |
| **Enforcement** | `HandleExceptionAttribute.OnException` catch block |
| **Outcome** | Exception swallowed; redirect still evaluated |
| **Origin** | code |
| **Source** | `src/Web.Mvc/ActionFilters/HandleExceptionAttribute.cs` |

---

## BR-14 AJAX requests are logged but not redirected

| Field | Value |
| --- | --- |
| **Statement** | AJAX requests are logged but not redirected; all other requests are redirected to the standard error page with the message. |
| **Trigger** | Unhandled action exception |
| **Enforcement** | `HandleExceptionAttribute.OnException` finally block, `Constants.ErrorUrl` |
| **Outcome** | `Response.Redirect(string.Format("{0}?MSG={1}", Constants.ErrorUrl, message))` (`Constants.ErrorUrl` is `//saifonline/weberror/processerror.aspx`) |
| **Origin** | code |
| **Source** | `src/Web.Mvc/ActionFilters/HandleExceptionAttribute.cs` |

---

## BR-15 Authorization filter requires its injected services

| Field | Value |
| --- | --- |
| **Statement** | The authorization filter requires its injected services to be present. |
| **Trigger** | Authorization filter stage |
| **Enforcement** | `SaifAuthorizeAttribute.AuthorizeCore` / `HandleUnauthorizedRequest` |
| **Outcome** | Argument exception thrown |
| **Origin** | code |
| **Source** | `src/Web.Mvc/ActionFilters/SaifAuthorizeAttribute.cs` |

---

## BR-16 No roles declared authorizes everyone

| Field | Value |
| --- | --- |
| **Statement** | An authorize attribute declared with no roles authorizes every caller. |
| **Trigger** | Authorization filter stage |
| **Enforcement** | `SaifAuthorizeAttribute.AuthorizeCore` |
| **Outcome** | Returns `true` |
| **Origin** | code |
| **Source** | `src/Web.Mvc/ActionFilters/SaifAuthorizeAttribute.cs` |

---

## BR-17 No resolvable system user is denied

| Field | Value |
| --- | --- |
| **Statement** | A request with no resolvable system user is denied. |
| **Trigger** | Authorization filter stage |
| **Enforcement** | `SaifAuthorizeAttribute.AuthorizeCore` |
| **Outcome** | Returns `false` |
| **Origin** | code |
| **Source** | `src/Web.Mvc/ActionFilters/SaifAuthorizeAttribute.cs` |

---

## BR-18 Access granted on any satisfied role

| Field | Value |
| --- | --- |
| **Statement** | Access is granted when the user is authorized for any one of the declared roles; `forceProductionRole` forces evaluation against the production resource name. |
| **Trigger** | Authorization filter stage |
| **Enforcement** | `SaifAuthorizeAttribute.AuthorizeCore` calling `IApplicationUserProfile.IsAuthorized(role, forceProductionRole)` |
| **Outcome** | Returns `true` on first satisfied role |
| **Origin** | delegated |
| **Source** | `src/Web.Mvc/ActionFilters/SaifAuthorizeAttribute.cs` |

---

## BR-19 Authorization denials are logged as a security exception

| Field | Value |
| --- | --- |
| **Statement** | Every authorization denial is logged as a security exception naming the user. |
| **Trigger** | Authorization denied |
| **Enforcement** | `SaifAuthorizeAttribute.HandleUnauthorizedRequest` |
| **Outcome** | `LogService.LogException(applicationName, controllerAndActionName, state: context, exception: new SecurityException("The user '{userId}' is not authorized for this view."))` |
| **Origin** | code |
| **Source** | `src/Web.Mvc/ActionFilters/SaifAuthorizeAttribute.cs` |

---

## BR-20 Denied users are redirected to the standard error page

| Field | Value |
| --- | --- |
| **Statement** | A denied user is redirected to the standard error page with a "Not Authorized" message. |
| **Trigger** | Authorization denied |
| **Enforcement** | `SaifAuthorizeAttribute.HandleUnauthorizedRequest` |
| **Outcome** | `RedirectResult("//saifonline/weberror/processerror.aspx?MSG=Not%20Authorized")` |
| **Origin** | code |
| **Source** | `src/Web.Mvc/ActionFilters/SaifAuthorizeAttribute.cs` |

---

## BR-21 Business Online menu is cached once per session

| Field | Value |
| --- | --- |
| **Statement** | The Business Online menu is retrieved once per session and cached; subsequent requests reuse the cached view model. |
| **Trigger** | `MenuControllerBase.OnActionExecuted` |
| **Enforcement** | `MenuControllerBase.Menu`, session key `BusinessOnlineMenu` |
| **Outcome** | Database query avoided on cache hit |
| **Origin** | code |
| **Source** | `src/Web.Mvc/Controllers/MenuControllerBase.cs` |

---

## BR-22 Active menu item identified by app and page link match

| Field | Value |
| --- | --- |
| **Statement** | The active menu item is identified by matching the current application and page link against the cached menu. |
| **Trigger** | `MenuControllerBase.OnActionExecuted` |
| **Enforcement** | `MenuControllerBase.GetCurrentItemId` |
| **Outcome** | `Menu.CurrentItemId` set, or `""` when unmatched |
| **Origin** | code |
| **Source** | `src/Web.Mvc/Controllers/MenuControllerBase.cs` |

---

## BR-23 External/internal user status resolved once per request

| Field | Value |
| --- | --- |
| **Statement** | The current user's external/internal status is resolved once per request and reused. |
| **Trigger** | `MenuControllerBase.OnActionExecuted` |
| **Enforcement** | `MenuControllerBase.ApplicationUser` memoized property |
| **Outcome** | `Menu.IsExternalUser` set from `IApplicationUserProfile.IsExternal` |
| **Origin** | code |
| **Source** | `src/Web.Mvc/Controllers/MenuControllerBase.cs` |

---

## BR-24 Route identifiers excluded from the menu lookup key

| Field | Value |
| --- | --- |
| **Statement** | Route identifiers are excluded from the menu lookup key — only the first two path segments participate. |
| **Trigger** | `GetCurrentItemId` |
| **Enforcement** | `MenuControllerBase.GetCurrentItemId` |
| **Outcome** | `Controller/Action` used as the lookup link |
| **Origin** | code |
| **Source** | `src/Web.Mvc/Controllers/MenuControllerBase.cs` |

---

## BR-25 Root request maps to Home/Index

| Field | Value |
| --- | --- |
| **Statement** | A root request for the current application maps to the `Home/Index` menu link, and a single-segment link is completed with `/Index`. |
| **Trigger** | `GetCurrentItemId` |
| **Enforcement** | `MenuControllerBase.GetCurrentItemId` |
| **Outcome** | Normalized page link |
| **Origin** | code |
| **Source** | `src/Web.Mvc/Controllers/MenuControllerBase.cs` |

---

## BR-26 Domain-qualified mail id reduced before lookup

| Field | Value |
| --- | --- |
| **Statement** | A domain-qualified mail id is reduced to the account name before menu lookup. |
| **Trigger** | `GetBusinessOnlineMenu` |
| **Enforcement** | `MenuService.GetBusinessOnlineMenu` |
| **Outcome** | `DOMAIN\user` becomes `user` |
| **Origin** | code |
| **Source** | `src/Web.Mvc/Classes/MenuService.cs` |

---

## BR-27 Every user also receives the shared UNSECURED menu

| Field | Value |
| --- | --- |
| **Statement** | Every user additionally receives the shared `UNSECURED` menu entries. |
| **Trigger** | `GetBusinessOnlineMenu` |
| **Enforcement** | `MenuService.GetBusinessOnlineMenu` |
| **Outcome** | Filter set `{ USERID, UNSECURED }` |
| **Origin** | code |
| **Source** | `src/Web.Mvc/Classes/MenuService.cs` |

---

## BR-28 Only visible menu rows are returned

| Field | Value |
| --- | --- |
| **Statement** | Only menu rows flagged visible (`IsVisible == "Y"`) are returned. |
| **Trigger** | `GetBusinessOnlineMenu` |
| **Enforcement** | `MenuService.GetBusinessOnlineMenu` query predicate |
| **Outcome** | Hidden rows excluded |
| **Origin** | code |
| **Source** | `src/Web.Mvc/Classes/MenuService.cs` |

---

## BR-29 Menu rows are ordered by sort order

| Field | Value |
| --- | --- |
| **Statement** | Menu rows are ordered by their configured sort order. |
| **Trigger** | `GetBusinessOnlineMenu` |
| **Enforcement** | `MenuService.GetBusinessOnlineMenu` `OrderBy(x => x.SortOrder)` |
| **Outcome** | Deterministic menu ordering |
| **Origin** | code |
| **Source** | `src/Web.Mvc/Classes/MenuService.cs` |

---

## BR-30 Favorite flag partitions rows into favorites and items

| Field | Value |
| --- | --- |
| **Statement** | Rows flagged `IsFavorite == "Y"` become favorites and rows flagged `"N"` become standard items; any other value places the row in neither collection. |
| **Trigger** | `GetBusinessOnlineMenu` |
| **Enforcement** | `MenuService.GetBusinessOnlineMenu` partition |
| **Outcome** | Row silently omitted when the flag is neither `Y` nor `N` |
| **Origin** | code |
| **Source** | `src/Web.Mvc/Classes/MenuService.cs` |

---

## BR-31 Menu data is read from the Oracle MENU schema

| Field | Value |
| --- | --- |
| **Statement** | Menu data is read from the Oracle `MENU` schema resolved through the environment's Oracle database configuration. |
| **Trigger** | Container build |
| **Enforcement** | `AutofacExtensions.RegisterCommonDependencies` |
| **Outcome** | `DbConnectionManager.GetOracleConnectionString(env, EnvironmentConfigKeys.OracleDatabase, "MENU")` |
| **Origin** | code |
| **Source** | `src/Web.Mvc/Extensions/AutofacExtensions.cs` |

---

## BR-32 Web context repository created once per session

| Field | Value |
| --- | --- |
| **Statement** | The web context repository is created once per session. |
| **Trigger** | First `WebContextService` resolution in a session |
| **Enforcement** | `WebContextService` constructor |
| **Outcome** | Session key `WebContextRepository` initialized |
| **Origin** | code |
| **Source** | `src/Web/WebContextService.cs` |

---

## BR-33 Context service endpoint resolved once per session

| Field | Value |
| --- | --- |
| **Statement** | The web context service endpoint URL is resolved from configuration once per session. |
| **Trigger** | First `WebContextService` resolution in a session |
| **Enforcement** | `WebContextService` constructor |
| **Outcome** | Session key `ContextServiceUrl` set from `EnvironmentConfigKeys.WebContextServiceUrl` |
| **Origin** | code |
| **Source** | `src/Web/WebContextService.cs` |

---

## BR-34 Pending-change grid keys are not correlated to a row

| Field | Value |
| --- | --- |
| **Statement** | Model-state keys belonging to a grid's pending-change collections (`.Added[`, `.Deleted[`, `.Updated[`) are not correlated to a grid row. |
| **Trigger** | `GetErrors` |
| **Enforcement** | `ModelStateExtensions` |
| **Outcome** | `GridRowId` left empty for those errors |
| **Origin** | code |
| **Source** | `src/Web.Mvc/Extensions/ModelStateExtensions.cs` |

---

## BR-35 Grid error key normalized before the row id is read

| Field | Value |
| --- | --- |
| **Statement** | A grid error key is normalized to the row's identifier property before the row id is read, inserting `.Rows` when the key targets the domain model. |
| **Trigger** | `GetErrors` |
| **Enforcement** | `ModelStateExtensions.GridRowKey` |
| **Outcome** | Normalized model-state key |
| **Origin** | code |
| **Source** | `src/Web.Mvc/Extensions/ModelStateExtensions.cs` |

---

## BR-36 Error keys are DOM-safe

| Field | Value |
| --- | --- |
| **Statement** | Error keys are DOM-safe: every `.` becomes `_`. |
| **Trigger** | `GetErrors` |
| **Enforcement** | `ModelStateExtensions.GetErrors` |
| **Outcome** | `ErrorViewModel.Key` |
| **Origin** | code |
| **Source** | `src/Web.Mvc/Extensions/ModelStateExtensions.cs` |

---

## BR-37 Duplicate error message displayed once but still flags the field

| Field | Value |
| --- | --- |
| **Statement** | A duplicate error message is added with blank text so it is displayed only once while still flagging the field. |
| **Trigger** | `AddError` |
| **Enforcement** | `ModelStateWrapper.AddError` |
| **Outcome** | Model-state entry created with empty message |
| **Origin** | code |
| **Source** | `src/Web.Mvc/Classes/ModelStateWrapper.cs` |

---

## BR-38 Duplicate warnings and informational messages are suppressed

| Field | Value |
| --- | --- |
| **Statement** | Duplicate warning and informational messages are not added at all. |
| **Trigger** | `AddWarning` / `AddInformation` |
| **Enforcement** | `ModelStateWrapper` |
| **Outcome** | Message suppressed |
| **Origin** | code |
| **Source** | `src/Web.Mvc/Classes/ModelStateWrapper.cs` |

---

## BR-39 Missing date value binds to DateTime default

| Field | Value |
| --- | --- |
| **Statement** | A missing date value binds to `DateTime` default rather than raising a binding error. |
| **Trigger** | Model binding |
| **Enforcement** | `JsonDateTimeModelBinder.BindModel` |
| **Outcome** | `0001-01-01` returned |
| **Origin** | code |
| **Source** | `src/Web.Mvc/ModelBinders/JsonDateTimeModelBinder.cs` |

---

## BR-40 ASP.NET JSON date form is converted from Unix epoch

| Field | Value |
| --- | --- |
| **Statement** | A value starting with `/` is interpreted as the ASP.NET JSON date `/Date(ms)/` and converted from the Unix epoch to local time. |
| **Trigger** | Model binding |
| **Enforcement** | `JsonDateTimeModelBinder.BindModel` |
| **Outcome** | Local `DateTime` returned |
| **Origin** | code |
| **Source** | `src/Web.Mvc/ModelBinders/JsonDateTimeModelBinder.cs` |

---

## BR-41 Unparseable date binds to the DateTime default

| Field | Value |
| --- | --- |
| **Statement** | An unparseable date binds to the `DateTime` default instead of failing. |
| **Trigger** | Model binding |
| **Enforcement** | `JsonDateTimeModelBinder.BindModel` |
| **Outcome** | `0001-01-01` returned |
| **Origin** | code |
| **Source** | `src/Web.Mvc/ModelBinders/JsonDateTimeModelBinder.cs` |

---

## BR-42 Favorite display text falls back to page name

| Field | Value |
| --- | --- |
| **Statement** | A favorite's display text falls back to the page name when no favorite name is configured. |
| **Trigger** | Menu render |
| **Enforcement** | `MenuExtensions.FavoriteLink` |
| **Outcome** | Link text resolved |
| **Origin** | code |
| **Source** | `src/Web.Mvc/Extensions/MenuExtensions.cs` |

---

## BR-43 Menu link title attribute is conditional

| Field | Value |
| --- | --- |
| **Statement** | A menu link emits a `title` attribute only when a tooltip is configured; favorites prefix it with the menu category. |
| **Trigger** | Menu render |
| **Enforcement** | `MenuExtensions.FavoriteLink` / `MenuLink` |
| **Outcome** | `title` attribute present or omitted |
| **Origin** | code |
| **Source** | `src/Web.Mvc/Extensions/MenuExtensions.cs` |

---

## BR-44 Menu items ordered by category and grouped

| Field | Value |
| --- | --- |
| **Statement** | Menu items are ordered by category and grouped by category code, with each group collapsed by default. |
| **Trigger** | Menu render |
| **Enforcement** | `MenuExtensions.RenderMenu` |
| **Outcome** | Nested `<ul class="nav nav-list is-hidden">` per category |
| **Origin** | code |
| **Source** | `src/Web.Mvc/Extensions/MenuExtensions.cs` |

---

## BR-45 Top menu shows only the controller's declared category

| Field | Value |
| --- | --- |
| **Statement** | The top menu shows only the items belonging to the controller's declared menu category, compared case-insensitively. |
| **Trigger** | Top menu render |
| **Enforcement** | `MenuExtensions.RenderTopMenu` |
| **Outcome** | Filtered item list |
| **Origin** | code |
| **Source** | `src/Web.Mvc/Extensions/MenuExtensions.cs` |

---

## BR-46 Web context control hidden unless explicitly enabled

| Field | Value |
| --- | --- |
| **Statement** | The web context control is rendered hidden unless the view model explicitly enables it; it is enabled by default. |
| **Trigger** | Web context render |
| **Enforcement** | `WebContextExtensions.RenderWebContext` |
| **Outcome** | `style="display: none;"` applied or omitted |
| **Origin** | code |
| **Source** | `src/Web.Mvc/Extensions/WebContextExtensions.cs` |

---

## BR-47 Generated element ids sanitize dots to underscores

| Field | Value |
| --- | --- |
| **Statement** | Generated element ids replace `.` with `_` so nested model paths remain valid DOM ids. |
| **Trigger** | Helper render |
| **Enforcement** | `LinkedDropdownHelper`, `TextCounterHelper` |
| **Outcome** | Sanitized `id` attribute |
| **Origin** | code |
| **Source** | `src/Web.Mvc/UI/Extensions/LinkedDropdownHelper.cs` |

---

## BR-48 Caller html attributes merge by concatenation

| Field | Value |
| --- | --- |
| **Statement** | Caller-supplied html attributes are merged into helper-generated attributes by concatenation, not replacement. |
| **Trigger** | Helper render |
| **Enforcement** | `ObjectExtensions.MergeAttributes` |
| **Outcome** | Combined attribute values |
| **Origin** | code |
| **Source** | `src/Web.Mvc/UI/Extensions/ObjectExtensions.cs` |

---

## BR-49 Linked dropdown option carries parent value as CSS class

| Field | Value |
| --- | --- |
| **Statement** | A linked dropdown option carries its parent value as a CSS class so the client script can filter it. |
| **Trigger** | Linked dropdown render |
| **Enforcement** | `LinkedDropdownHelper.LinkedDropdownList` |
| **Outcome** | `<option class="{LinkValue}">` |
| **Origin** | code |
| **Source** | `src/Web.Mvc/UI/Extensions/LinkedDropdownHelper.cs` |

---

## BR-50 Default option suppressed when requested

| Field | Value |
| --- | --- |
| **Statement** | When `removeDefault` is requested, the option with value `-1` is omitted. |
| **Trigger** | Linked dropdown render |
| **Enforcement** | `LinkedDropdownHelper.LinkedDropdownList` |
| **Outcome** | Default option suppressed |
| **Origin** | code |
| **Source** | `src/Web.Mvc/UI/Extensions/LinkedDropdownHelper.cs` |

---

## BR-51 Selected-value matching is case-insensitive

| Field | Value |
| --- | --- |
| **Statement** | Selected-value matching for linked select lists is case-insensitive. |
| **Trigger** | `GetListItems` |
| **Enforcement** | `LinkedSelectList.GetListItems` |
| **Outcome** | `LinkedSelectListItem.Selected` set |
| **Origin** | code |
| **Source** | `src/Web.Mvc/UI/Extensions/LinkedDropdownHelper.cs` |

---

## BR-52 Strongly-typed textarea counter always uses the 4000 default

| Field | Value |
| --- | --- |
| **Statement** | The strongly-typed textarea counter always applies the default maximum length of 4000 characters. |
| **Trigger** | `TextAreaCounterFor` |
| **Enforcement** | `TextCounterHelper.TextAreaCounterFor` |
| **Outcome** | `data-max-length="4000"` |
| **Origin** | code |
| **Source** | `src/Web.Mvc/UI/Extensions/TextCounterHelper.cs` |

---

## BR-53 Textarea counter without label text renders a bare span

| Field | Value |
| --- | --- |
| **Statement** | A textarea counter with no label text renders a bare `<span>` instead of a `<label>`. |
| **Trigger** | Textarea counter render |
| **Enforcement** | `TextCounterHelper.TextAreaCounter` |
| **Outcome** | `<span></span>` emitted |
| **Origin** | code |
| **Source** | `src/Web.Mvc/UI/Extensions/TextCounterHelper.cs` |

---

## BR-54 Standard save button uses a fixed id, icon, and tooltip

| Field | Value |
| --- | --- |
| **Statement** | The standard save toolbar button uses the fixed id `lnk-save`, save icon, and "Save (Alt + S)" tooltip. |
| **Trigger** | `AddSaveButton` |
| **Enforcement** | `ToolbarItemFactory.AddSaveButton` |
| **Outcome** | Pre-populated `ToolbarItem` |
| **Origin** | code |
| **Source** | `src/Web.Mvc/UI/Toolbar/ToolbarItemFactory.cs` |

---

## BR-55 Non-production servers use the Test Glimpse resource variant

| Field | Value |
| --- | --- |
| **Statement** | Non-production servers use the `Test` variant of the Glimpse resource name; a machine name containing `prod` uses the production name. |
| **Trigger** | `HasGlimpseAccess` |
| **Enforcement** | `SAIF.Web.HttpContextExtensions.HasGlimpseAccess` |
| **Outcome** | `GlimpseUserTestResource` or `GlimpseUserResource` |
| **Origin** | code |
| **Source** | `src/Web/Extensions/HttpContextExtensions.cs` |

---

## BR-56 Glimpse access requires an authenticated user and a negated role test

| Field | Value |
| --- | --- |
| **Statement** | Glimpse access requires an authenticated user and is decided by the `GlimpseUser{env}Resource` role membership test.* |
| **Trigger** | `HasGlimpseAccess` |
| **Enforcement** | `SAIF.Web.HttpContextExtensions.HasGlimpseAccess` |
| **Outcome** | Boolean returned; the role test is negated in the current code |
| **Origin** | code |
| **Source** | `src/Web/Extensions/HttpContextExtensions.cs` |

---

## BR-57 Common style bundle always includes the toolbar stylesheet

| Field | Value |
| --- | --- |
| **Statement** | The common style bundle always includes the Business Online toolbar stylesheet. |
| **Trigger** | Bundle registration |
| **Enforcement** | `BundleExtensions.RegisterCommonStyles` |
| **Outcome** | `toolbar.css` included |
| **Origin** | code |
| **Source** | `src/Web.Mvc/Extensions/BundleTableExtensions.cs` |

---

## BR-58 Kendo styles are version-scoped and always include common/default

| Field | Value |
| --- | --- |
| **Statement** | Kendo styles are version-scoped by folder and always include `kendo.common.min` and `kendo.default.min`. |
| **Trigger** | Bundle registration |
| **Enforcement** | `BundleExtensions.RegisterKendoStyles` |
| **Outcome** | Versioned paths included |
| **Origin** | code |
| **Source** | `src/Web.Mvc/Extensions/BundleTableExtensions.cs` |

---

## BR-59 Bundle file names get .css appended automatically

| Field | Value |
| --- | --- |
| **Statement** | Bundle file names are supplied without extension; `.css` is appended by the helper. |
| **Trigger** | Bundle registration |
| **Enforcement** | `BundleExtensions.IncludeMany` |
| **Outcome** | `{baseUrl}/{file}.css` |
| **Origin** | code |
| **Source** | `src/Web.Mvc/Extensions/BundleTableExtensions.cs` |

---

## BR-60 Label text precedence order

| Field | Value |
| --- | --- |
| **Statement** | Label text precedence is explicit text, then display name, then property name, then the final segment of the field name. |
| **Trigger** | `LabelFor` |
| **Enforcement** | `LabelExtensions.LabelHelper` |
| **Outcome** | Resolved inner text |
| **Origin** | code |
| **Source** | `src/Web.Mvc/Extensions/LabelExtensions.cs` |

---

## BR-61 Label with no resolvable text renders nothing

| Field | Value |
| --- | --- |
| **Statement** | A label with no resolvable text renders nothing. |
| **Trigger** | `LabelFor` |
| **Enforcement** | `LabelExtensions.LabelHelper` |
| **Outcome** | `MvcHtmlString.Empty` |
| **Origin** | code |
| **Source** | `src/Web.Mvc/Extensions/LabelExtensions.cs` |

---

## BR-62 Default context value only seeded when a referrer exists

| Field | Value |
| --- | --- |
| **Statement** | A default context value is only seeded when the request has a referrer. |
| **Trigger** | `SetDefaultCurrentValue` |
| **Enforcement** | `IWebContextExtensions.SetDefaultCurrentValue` |
| **Outcome** | No change when the referrer is absent |
| **Origin** | code |
| **Source** | `src/Web/WebContextService.cs` |

---

## BR-63 Default context value seeded from the first non-empty candidate

| Field | Value |
| --- | --- |
| **Statement** | A default context value is seeded from the first non-empty candidate value when the referring application differs from the current one. |
| **Trigger** | `SetDefaultCurrentValue` |
| **Enforcement** | `IWebContextExtensions.SetDefaultCurrentValue` |
| **Outcome** | `context.CurrentValue` set to the first non-empty candidate |
| **Origin** | code |
| **Source** | `src/Web/WebContextService.cs` |

---

## BR-64 Clearing a grid resets only pending-change collections

| Field | Value |
| --- | --- |
| **Statement** | Clearing a grid resets only the pending-change collections; the displayed rows are preserved. |
| **Trigger** | `ClearAll` |
| **Enforcement** | `GridRowViewModelExtensions.ClearAll` |
| **Outcome** | `Added`, `Deleted`, `Updated` emptied |
| **Origin** | code |
| **Source** | `src/Web.Mvc/ViewModels/GridRowViewModel.cs` |

---

## Notes
- `Origin: delegated` rules are decided by a dependency this library invokes (`SAIF.Framework.Core`'s `Enforce` guard helper and `IApplicationUserProfile.IsAuthorized`); this library owns the call site, not the decision logic.
- Mark inferred statements with `*` and unknown statements with `**`. Do not use bold inline confidence labels.

> **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.

## 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)
- [API reference](api/index.md)

## Sources
- `src/Web.Mvc/ActionFilters/HandleExceptionAttribute.cs`
- `src/Web.Mvc/ActionFilters/SaifAuthorizeAttribute.cs`
- `src/Web.Mvc/Classes/MenuService.cs`
- `src/Web.Mvc/Classes/ModelStateWrapper.cs`
- `src/Web.Mvc/Controllers/MenuControllerBase.cs`
- `src/Web.Mvc/Controllers/WebContextControllerBase.cs`
- `src/Web.Mvc/Extensions/AutofacExtensions.cs`
- `src/Web.Mvc/Extensions/BundleTableExtensions.cs`
- `src/Web.Mvc/Extensions/HttpContextExtensions.cs`
- `src/Web.Mvc/Extensions/LabelExtensions.cs`
- `src/Web.Mvc/Extensions/MenuExtensions.cs`
- `src/Web.Mvc/Extensions/ModelStateExtensions.cs`
- `src/Web.Mvc/Extensions/TempDataExtensions.cs`
- `src/Web.Mvc/Extensions/WebContextExtensions.cs`
- `src/Web.Mvc/ModelBinders/JsonDateTimeModelBinder.cs`
- `src/Web.Mvc/UI/Extensions/LinkedDropdownHelper.cs`
- `src/Web.Mvc/UI/Extensions/ObjectExtensions.cs`
- `src/Web.Mvc/UI/Extensions/TextCounterHelper.cs`
- `src/Web.Mvc/UI/Toolbar/ToolbarItemFactory.cs`
- `src/Web.Mvc/ViewModels/GridRowViewModel.cs`
- `src/Web.Mvc/ViewModels/WebContextViewModel.cs`
- `src/Web/Constants.cs`
- `src/Web/Extensions/HttpContextExtensions.cs`
- `src/Web/WebContextService.cs`
