Skip to content

Example: ASP.NET Core Razor Pages application architecture document

Doc gen writes docs/application/architecture.md for an ASP.NET Core Razor Pages front end from Program.cs, the Startup class, the route conventions, the page-model chain, the composition root, and the layouts and static assets the pages load. It is the page that screen documents cite for inherited behavior, so the behavior a BolMiddleware or SAIF.NCore base class contributes is stated here once and marked, not rewritten on every screen. The example is for the same fictional SAIF.Policy.Portal application as the Razor Pages screen example and the hosted endpoints example.

What reviewers should check

  • Start-up is in source order. ## Host start-up lists Program.cs and Startup calls one row at a time, states the hosting model (here a minimal host driving Startup by hand), and records what the base class contributes as one row marked †.
  • Package behavior is marked, not asserted. Anything contributed by BolRazorLayoutStartup, the page-model chain, the layouts or the context resolution carries † or **. The same text without a marker, or behavior with no package documentation or source behind it, is a defect.
  • The middleware order shows where base.Configure sits. Its position relative to UsePathBase and the application's own middleware is the finding, not the list of middleware inside the base call.
  • The route table records only what changes the file path. AddPageRoute conventions, @page templates that differ from the file name, the base path, and any MapControllers or MapGet registration. File-path routes are not listed one by one.
  • The page-model hierarchy is a chain, then a table. The Mermaid classDiagram shows the BolMiddleware chain and the concrete page models; the table gives each model's base, roles and the screen it serves.
  • The composition root is a recorded path. ## Composition root names the file the run recorded, summarizes base.ConfigureServices, and lists lifetimes. A companion project is part of the application and is documented here, not under docs/libraries/.
  • Anti-forgery is stated with its source. The page says whether POST handlers are validated and what declares it (here the Razor Pages default, with no [IgnoreAntiforgeryToken] anywhere).
  • No WebForms or MVC 5 vocabulary. Global.asax, RouteConfig, SaifAuthorize, ActionResult, Controller/Action, postback and .aspx do not appear.
  • Related documentation links root pages only. Links go to the application pages and indexes, never to per-item pages such as an individual operation or screen page, and a page that was not written is not linked.
  • Diagrams use standard fences. Both diagrams are ```mermaid fences.

Example

The document below is a complete, correct architecture.md for a fictional application.

View rendered example — the same document as a real page, so you can compare rendering as well as source.

---
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](../libraries/saif-policy-billing/overview.md) | 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](../screens/policy-search.md) |
| `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](../screens/payroll-reports.md) |
| `NewModel` | `BolPolicyModel`† | PolicyServices.Underwriting | [New Endorsement](../screens/endorsement-new.md) |
| `LedgerModel` | `BolPolicyModel`† | PolicyServices.Read, PolicyServices.Underwriting | [Premium Ledger](../screens/premium-ledger.md) |

## 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](../libraries/saif-policy-billing/overview.md) | 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](../screens/policy-search.md) |
| `wwwroot/js/policy/summary.js` | [Policy Summary](../screens/policy-summary.md) |
| `wwwroot/js/payroll/reports.js` | [Payroll Reports](../screens/payroll-reports.md) |

## 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](../libraries/saif-policy-billing/overview.md) |
| 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](index.md)
- [Overview](overview.md)
- [Dependencies](dependencies.md)
- [Configuration](configuration.md)
- [Hosted endpoints](hosted-endpoints.md)
- [Operations and processes](operations-processes.md)
- [Business rules](business-rules.md)
- [Security and authorization](security-authorization.md)
- [Data model](data-model.md)
- [Tech debt](tech-debt.md)

---

> [!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