---
title: Application architecture
summary: Layer diagram, host start-up, routing, page-model hierarchy, composition root and client-side stack of the Razor Pages application.
status: draft
owner: Policy Services Team
last_reviewed: 2026-09-09
---

# Application architecture

The Policy Services Portal is an ASP.NET Core 8.0 Razor Pages application on .NET 8, hosted in IIS in-process under the `/PolicyPortal` base path. Its `Startup` class derives from the SAIF `BolRazorLayoutStartup` start-up base class†, so authentication, page chrome, menu and policy context come from the BolMiddleware package† rather than from application source. The application contributes the pages, the page models that derive from the BolMiddleware chain†, a small set of application services, and the Kendo UI front end. It has no database of its own: business data is reached through SAIF API clients and the PolicyCore, Billing and document-rendering APIs behind them.

## Layer diagram

```mermaid
flowchart TD
    Browser["Browser: Razor Pages, Kendo UI, page script modules"] --> Pages[Razor Pages and page models]
    Pages --> Base["BolMiddleware page models and layouts"]
    Pages --> Services[Application services]
    Base --> BolApis[BOL Context, Menu, Policy Header and Security APIs]
    Services --> Clients[SAIF API clients]
    Clients --> Apis[Back-end APIs]
```

## Layer descriptions

| Layer | Projects / packages | Responsibility |
| --- | --- | --- |
| Presentation | `SAIF.Policy.Portal` (`Pages\`, `wwwroot\`) | Razor Pages, page models, partial views and page script modules; no business logic |
| Page chrome and context | `BolMiddleware`† | Layouts, view components, web context, menu and policy header resolution, and the `BolContextModel` to `BolPolicyModel` page-model chain |
| Host and authentication | `SAIF.NetCore.ServiceExtensions`† | OIDC sign-in through the `OidcRazorStartup` base of `BolRazorLayoutStartup` |
| Application services | `SAIF.Policy.Portal` (`Services\`) | `UserPermissionService`, which wraps the security service for in-handler permission checks, and AutoMapper profiles for view models |
| Billing | SAIF.Policy.Billing | Premium balance operations that the screens call, such as refreshing a balance |
| Policy data and documents | `SAIF.Policy.Services`†, `SAIF.Policy.Documents`† | Policy summary retrieval and declaration page rendering through the PolicyCore and document-rendering APIs |

## Host start-up

The application uses a minimal host: `Program.cs` creates the `WebApplication` builder and drives a `Startup` instance by hand rather than calling `UseStartup<T>`.

| # | Call | Effect | Source |
| --- | --- | --- | --- |
| 1 | `WebApplication.CreateBuilder(args)` | Creates the builder and loads `appsettings.json`, `appsettings.<Environment>.json` and environment variables | `Program.cs:5` |
| 2 | `new Startup(builder.Configuration)` | The constructor sets `EnableNewtonsoftJsonSerializer` and `EnableSessionMiddleware` to `true`; both are switches on `BolRazorLayoutStartup`† | `Program.cs:6`, `Startup.cs:18` |
| 3 | `startup.ConfigureServices(builder.Services)` | Runs the registrations in rows 4 to 9 | `Program.cs:7` |
| 4 | `base.ConfigureServices(services)` | Inherited registrations from `BolRazorLayoutStartup`†: OIDC authentication, the `IBolServicesContainer` service set, and the `BasicAppMap` section bound to `IAppMap`; called first so the application registrations that follow can replace them | `Startup.cs:26` |
| 5 | `services.AddRazorPages(...)` | Adds Razor Pages and the `AddPageRoute("/Policy/Search", "")` convention that serves Policy Search at the site root | `Startup.cs:28` |
| 6 | `services.Configure<PortalSettings>(...)` | Binds the `AppSettings` section to `PortalSettings` | `Startup.cs:34` |
| 7 | `services.AddScoped<...>()` (four calls) | Registers the application services listed under [Composition root](#composition-root) | `Startup.cs:36` |
| 8 | `services.AddAutoMapper(typeof(Startup))` | Registers `IMapper` and every `Profile` in the assembly | `Startup.cs:41` |
| 9 | `services.AddMemoryCache()` | Registers `IMemoryCache` | `Startup.cs:42` |
| 10 | `builder.Build()` | Builds the `WebApplication` | `Program.cs:8` |
| 11 | `startup.Configure(app, app.Environment)` | Runs the middleware registrations listed under [Middleware order](#middleware-order) | `Program.cs:9` |
| 12 | `app.Run()` | Starts the host | `Program.cs:10` |

## Middleware order

`Startup.Configure` registers three calls of its own and then calls the base class last.

| # | Middleware | Effect | Source |
| --- | --- | --- | --- |
| 1 | `UsePathBase("/PolicyPortal")` | Strips the `/PolicyPortal` base path so every later middleware and the route table see the path without it | `Startup.cs:48` |
| 2 | `UseDeveloperExceptionPage()` in Development, otherwise `UseExceptionHandler("/Error")` | Shows the developer exception page locally and the stock `/Error` page elsewhere | `Startup.cs:50` |
| 3 | `UseMiddleware<CorrelationIdMiddleware>()` | Reads or creates the `X-Correlation-Id` value, returns it as a response header and adds it to the log scope | `Startup.cs:59` |
| 4 | `base.Configure(app, env)` | Inherited pipeline†: session (enabled by the constructor switch), static files, routing, authentication, authorization and Razor Pages endpoints; called last, so rows 1 to 3 run ahead of it | `Startup.cs:61` |

## Route table

Routes come from the file path under `Pages\`. The conventions and route templates below change that default. Every URL is relative to the `/PolicyPortal` base path set at middleware step 1. The application registers no `MapControllers` or `MapGet` endpoints, so its AJAX requests are named page handlers, listed on [Hosted endpoints](hosted-endpoints.md). `/Error` and `/AccessDenied` have no route template and are served at their file URLs.

| # | Page | URL | Defined by | Source |
| --- | --- | --- | --- | --- |
| 1 | `/Policy/Search` | `/` | `Conventions.AddPageRoute("/Policy/Search", "")` | `Startup.cs:30` |
| 2 | `/Policy/Summary` | `/Policy/Summary/{policyNumber?}` | `@page "{policyNumber?}"` | `Pages/Policy/Summary.cshtml:1` |
| 3 | `/Payroll/Reports` | `/Payroll/Reports/{policyNumber?}` | `@page "{policyNumber?}"` | `Pages/Payroll/Reports.cshtml:1` |
| 4 | `/Endorsement/New` | `/Endorsement/New/{policyNumber?}` | `@page "{policyNumber?}"` | `Pages/Endorsement/New.cshtml:1` |
| 5 | `/Premium/Ledger` | `/Premium/Ledger/{policyNumber?}` | `@page "{policyNumber?}"` | `Pages/Premium/Ledger.cshtml:1` |

## Page model hierarchy

```mermaid
classDiagram
    PageModel <|-- BolContextModel
    BolContextModel <|-- BolMenuModel
    BolMenuModel <|-- BolPolicyModel
    BolMenuModel <|-- SearchModel
    BolPolicyModel <|-- SummaryModel
    BolPolicyModel <|-- ReportsModel
    BolPolicyModel <|-- NewModel
    BolPolicyModel <|-- LedgerModel
```

`BolContextModel`, `BolMenuModel` and `BolPolicyModel` come from the BolMiddleware package†. They override the page-handler lifecycle hooks, so context resolution, menu and policy header loading, and the redirect to `/Policy/Search` when a policy page has no policy number in context run before every handler of a derived page†. No application page model overrides those hooks, and the application has no base page model of its own.

| Page model | Base | Roles | Serves |
| --- | --- | --- | --- |
| `SearchModel` | `BolMenuModel`† | PolicyServices.Read, PolicyServices.Underwriting | Policy Search |
| `SummaryModel` | `BolPolicyModel`† | PolicyServices.Read, PolicyServices.Underwriting; refresh balance is PolicyServices.Underwriting only | [Policy Summary](../screens/policy-summary.md) |
| `ReportsModel` | `BolPolicyModel`† | PolicyServices.Read, PolicyServices.Underwriting | Payroll Reports |
| `NewModel` | `BolPolicyModel`† | PolicyServices.Underwriting | New Endorsement |
| `LedgerModel` | `BolPolicyModel`† | PolicyServices.Read, PolicyServices.Underwriting | Premium Ledger |

## Layouts, view imports and middleware includes

The four BOL layouts, the three view components and `MenuNavigation.js` ship in the BolMiddleware package† and are not files in this repository.

| Item | Value | Source |
| --- | --- | --- |
| `_ViewStart` layout | `_PolicyLayout`† | `Pages/_ViewStart.cshtml:2` |
| Layout override | `_MenuOnlyLayout`† on `/Policy/Search`, which has no policy context | `Pages/Policy/Search.cshtml:3` |
| `_ViewImports` tag helpers | `Microsoft.AspNetCore.Mvc.TagHelpers`, `Kendo.Mvc` | `Pages/_ViewImports.cshtml:2` |
| `MiddlewareIncludes/siteStyles` | Adds the Kendo UI theme and the compiled `site.css` to every page | `Pages/MiddlewareIncludes/siteStyles.cshtml` |
| `MiddlewareIncludes/siteScripts` | Adds jQuery, the Bootstrap bundle, Kendo UI and `site.js` to every page | `Pages/MiddlewareIncludes/siteScripts.cshtml` |

## Composition root

**Location:** `src/SAIF.Policy.Portal/Startup.cs` (`ConfigureServices`)

`ConfigureServices` calls `base.ConfigureServices` first†, then adds Razor Pages with its page-route convention, binds `PortalSettings`, registers the four application services as scoped, and finishes with AutoMapper and the memory cache. The application registers no logging provider of its own. The services supplied by `base.ConfigureServices`, including `IBolServicesContainer`†, are not repeated in the table.

| Service | Implementation | Lifetime | Configuration used |
| --- | --- | --- | --- |
| `IPolicySummaryService` | `PolicySummaryService` in `SAIF.Policy.Services`† | Scoped | `AppSettings:PolicyCoreApiUrl` |
| `IDeclarationRenderer` | `DeclarationRenderer` in `SAIF.Policy.Documents`† | Scoped | `AppSettings:DocumentApiUrl` |
| `IPremiumBalanceService` | `PremiumBalanceService` in SAIF.Policy.Billing | Scoped | `AppSettings:BillingApiUrl` |
| `IUserPermissionService` | `UserPermissionService` | Scoped | None |
| `IMemoryCache` | `MemoryCache` | Singleton | None |

## Filters, conventions and tag helpers

| Kind | Type | Scope | Effect |
| --- | --- | --- | --- |
| Page filter | `OnPageHandlerExecuting` override† | `BolContextModel` and every derived page model | Resolves the web context and loads menu and policy header data before the handler runs; redirects to `/Policy/Search` when a policy page has no policy number |
| Page route convention | `AddPageRoute("/Policy/Search", "")` | `/Policy/Search` | Serves Policy Search at the site root as well as at its file URL |
| Authorization | `[SecurityApiAuthorize("PolicyServices.Read,PolicyServices.Underwriting")]` | `BolPolicyModel`† and `SearchModel` | Only users holding one of the two roles can open the page |
| Authorization | `[SecurityApiAuthorize("PolicyServices.Underwriting")]` | `SummaryModel.OnPostRefreshBalanceAsync` and `NewModel` | Only underwriting staff can refresh a balance or start an endorsement; others are redirected to `/AccessDenied` |
| Anti-forgery | Razor Pages default for unsafe verbs | every POST handler | Validates the request verification token. No page model declares `[IgnoreAntiforgeryToken]` and `Startup` registers no override, so every POST handler, including `OnPostRefreshBalanceAsync` and `OnPostReadAsync`, is validated |
| Tag helper set | `Kendo.Mvc` | application | Makes the Kendo UI tag helpers, such as `kendo-grid` and `kendo-dropdownlist`, available on every page |

## Client-side stack

| Asset | Contents | Loaded by |
| --- | --- | --- |
| `wwwroot/lib/kendo-ui/js/kendo.all.min.js` | Kendo UI 2024.1.130, local copy pinned in `libman.json` | `MiddlewareIncludes/siteScripts.cshtml` |
| `wwwroot/lib/jquery`, `wwwroot/lib/bootstrap` | jQuery 3.7.1 and the Bootstrap 5.3.3 bundle, local copies | `MiddlewareIncludes/siteScripts.cshtml` |
| `wwwroot/css/site.css` | Compiled from `wwwroot/scss/site.scss` through `gulpfile.js`; do not edit the compiled output | `MiddlewareIncludes/siteStyles.cshtml` |

| Script module | Screen |
| --- | --- |
| `wwwroot/js/policy/search.js` | Policy Search |
| `wwwroot/js/policy/summary.js` | [Policy Summary](../screens/policy-summary.md) |
| `wwwroot/js/payroll/reports.js` | Payroll Reports |

## Key patterns
- **Inherited context:** policy context, menu and header are resolved by the BolMiddleware page-model chain† and are cited from each screen as inherited behavior, not rewritten per page.
- **Handlers as the endpoint layer:** AJAX, grid and file requests are named page handlers on the page that uses them; there are no API controllers.
- **Thin page models:** handlers delegate to the application services above and map results to view models with AutoMapper.
- **Server-rendered partials:** dialog content is a partial view fetched on demand and injected into a container, not built in script.

## Integration points

| Integration | Type | Direction | Libraries involved |
| --- | --- | --- | --- |
| BOL Context, Menu, Policy Header and Security APIs | API | outbound | `BolMiddleware`† |
| PolicyCore API | API | outbound | `SAIF.Policy.Services`† |
| Billing API | API | outbound | SAIF.Policy.Billing |
| Document rendering API | API | outbound | `SAIF.Policy.Documents`† |
| OIDC identity provider | OIDC | outbound | `SAIF.NetCore.ServiceExtensions`† |

## Design constraints
- The application is served under the `/PolicyPortal` base path; script modules build URLs from the page or from `~/`, never from a root-relative literal.
- `base.ConfigureServices` is called first and `base.Configure` last, as the package requires†.
- A policy page needs a policy number in `WebContext`; the redirect when it is missing belongs to the base page model†.
- The application has no database access; all business data comes through the APIs above.
- `site.css` is compiled output; change `site.scss` instead.

## Related documentation
- Application index
- Overview
- Dependencies
- Configuration
- [Hosted endpoints](hosted-endpoints.md)
- Operations and processes
- Business rules
- Security and authorization
- Data model
- Tech debt

---

> [!NOTE]
> Statements marked with `*` are inferred from code patterns. Statements marked with `**` could not be verified from available source. Statements marked with `†` are sourced from package documentation retrieved with the Legends CLI.

## Sources
- src/SAIF.Policy.Portal/Program.cs
- src/SAIF.Policy.Portal/Startup.cs
- src/SAIF.Policy.Portal/SAIF.Policy.Portal.csproj
- src/SAIF.Policy.Portal/Pages/_ViewStart.cshtml
- src/SAIF.Policy.Portal/Pages/_ViewImports.cshtml
- src/SAIF.Policy.Portal/Pages/MiddlewareIncludes/siteScripts.cshtml
- src/SAIF.Policy.Portal/Pages/MiddlewareIncludes/siteStyles.cshtml
- src/SAIF.Policy.Portal/Pages/Policy/Summary.cshtml.cs
- src/SAIF.Policy.Portal/gulpfile.js
