# Architecture Overview

OtisEd.Nimble is a monolithic, multi-tenant enterprise educational portal built on ABP Framework 8.3.x and .NET 8.0 using Domain-Driven Design. A React 18 + TypeScript SPA is hosted within the ASP.NET Core application. The system serves multiple state-level education agencies (Kentucky, North Dakota, US Virgin Islands) as separate tenants with shared infrastructure and tenant-specific branding.

## Table of Contents

* [Layer Map](#layer-map)
* [Dependency Flow](#dependency-flow)
* [Request Lifecycle](#request-lifecycle)
* [Module System](#module-system)
* [Multi-Tenancy](#multi-tenancy)
* [Authentication Architecture](#authentication-architecture)
* [Frontend Integration](#frontend-integration)
* [Background Jobs](#background-jobs)
* [Cross-Cutting Infrastructure](#cross-cutting-infrastructure)

***

## Layer Map

The solution follows ABP's standard DDD layered structure. Each layer is a separate C# project.

```
Web (Host)
  |  ASP.NET Core hosting, Razor Pages, auth config, SPA middleware, Hangfire
  |
  +-- HttpApi (~40 controllers)
  |   REST API controllers; thin delegation to application services
  |
  +-- Application (~76 app services)
  |   Business orchestration, AutoMapper profiles, email templates
  |
  +-- Application.Contracts
  |   Service interfaces (IXxxAppService), DTOs, permission definitions
  |
  +-- Domain (~247 files)
  |   Entities, domain managers, repository interfaces
  |
  +-- Domain.Shared
  |   Enums, constants (*Consts classes), shared DTOs
  |
  +-- EntityFrameworkCore
  |   NimbleDbContext, EF Core repository implementations, migrations
  |
  +-- HttpApi.Client
  |   Typed HTTP client proxies for remote service calls
  |
  +-- DataSync
  |   Standalone data synchronization service (4 files)
  |
  +-- DbMigrator
      Console app for database migrations
```

### Custom Modules (under `modules/`)

| Module                | Purpose                                                                 |
| --------------------- | ----------------------------------------------------------------------- |
| `OtisEd.AiChatBot`    | Full ABP module structure; Azure OpenAI + Azure AI Search               |
| `Volo.FileManagement` | Forked ABP module; file/directory management with Azure Blob Storage    |
| `Volo.Forms`          | Forked ABP module; dynamic form builder (questions, choices, responses) |

***

## Dependency Flow

Dependencies flow strictly downward. Higher layers may depend on any layer below them; lower layers must not depend on higher layers.

```
Web
 -> HttpApi -> Application -> Domain -> Domain.Shared
                          -> Application.Contracts -> Domain.Shared
            -> EntityFrameworkCore -> Domain
```

Key enforcement points:

* `Domain` and `Domain.Shared` have no dependencies on ABP infrastructure beyond `Volo.Abp.Domain`
* `Application.Contracts` defines interfaces that `Application` implements — separating contract from implementation enables `HttpApi.Client` to proxy the same interface remotely
* `EntityFrameworkCore` is the only project with a SQL Server dependency; all other projects talk to repository interfaces

***

## Request Lifecycle

1. **Browser** — React SPA (served from `ReactApp/build` via SPA middleware) makes Axios HTTP requests to `api/app/*`
2. **Web layer** — XSRF-TOKEN cookie validated via `RequestVerificationToken` header; ABP middleware runs (tenant resolution, auth, audit)
3. **HttpApi controller** — Thin delegation; no business logic. Controller inherits `NimbleController`, implements the corresponding `IXxxAppService`, and forwards the call
4. **Application service** — Validates permissions, maps DTOs (AutoMapper), calls domain layer and repositories. Inherits `NimbleAppService`
5. **Domain layer** — Entities enforce invariants in constructors and `SetXxx()` methods using `Volo.Abp.Check`; domain managers orchestrate complex operations
6. **EntityFrameworkCore** — Repository implementations (`EfCoreXxxRepository`) execute queries against `NimbleDbContext`; SQL Server via connection string `"Default"`

***

## Module System

### Project Scale

| Project                               | File Count | Role                                             |
| ------------------------------------- | ---------- | ------------------------------------------------ |
| `OtisEd.Nimble.Domain`                | \~247      | Entities, domain services, repository interfaces |
| `OtisEd.Nimble.Application`           | \~76       | Application services                             |
| `OtisEd.Nimble.Application.Contracts` | \~80+      | DTOs, service interfaces, permissions            |
| `OtisEd.Nimble.HttpApi`               | \~40       | REST controllers                                 |
| `OtisEd.Nimble.EntityFrameworkCore`   | \~60+      | DbContext, repositories, migrations              |
| `OtisEd.Nimble.Domain.Shared`         | \~100+     | Enums, constants, shared DTOs                    |
| `OtisEd.Nimble.Web`                   | \~30+      | Hosting, auth, Razor Pages, middleware           |
| `ReactApp/src`                        | \~740      | React SPA                                        |

### ABP Module Registration

Each C# project defines an ABP module class (`[DependsOn(...)]`) that wires dependencies. The Web module (`NimbleWebModule`) is the composition root and registers all other modules, including the three custom modules and ABP framework modules (Identity, Saas, AuditLogging, FileManagement, Forms, etc.).

### Package Version Management

All shared NuGet package versions are centralized in `Directory.Build.props` at the solution root. Do not set package versions in individual `.csproj` files.

***

## Multi-Tenancy

### Mechanism

Multi-tenancy is provided by ABP's `Volo.Saas` module. Each tenant has an isolated `TenantId` on every multi-tenant entity. Tenant resolution happens in ABP middleware before the request reaches the application layer.

### Known Tenants

| Tenant Key | Description                                   |
| ---------- | --------------------------------------------- |
| KDE        | Kentucky Department of Education              |
| KYSRC      | Kentucky School Report Card                   |
| NDDPI      | North Dakota Department of Public Instruction |
| VIDE       | US Virgin Islands Department of Education     |

### Data Isolation

* Domain entities that implement `IMultiTenant` carry a `TenantId` (nullable `Guid`); `null` means host-level
* `NimbleDbContext` uses `[ConnectionStringName("Default")]` and replaces both `IIdentityProDbContext` and `ISaasDbContext`, enabling cross-module JOINs within a single context
* Separate connection strings per tenant are supported via `TenantConnectionString` entity
* Schema-level isolation is not used; all tenants share the same SQL Server schema with row-level `TenantId` filtering applied by ABP's EF Core interceptors
* `NimbleTenantDatabaseMigrationHandler` runs migrations per tenant on startup

### Entities NOT Multi-Tenant

`DcAgency`, `DcFileRequest`, `Glossary`, `Card`, `Page`, `CardData`, `StoreMessage` do not implement `IMultiTenant` — these are global/host-level data shared across tenants.

***

## Authentication Architecture

### Protocol Stack

* **OpenIddict** manages the OAuth 2.0 / OpenID Connect token lifecycle; configured with encryption and signing certificates loaded from `App:CertificatePassword`
* External identity providers are registered via `ConfigureExternalProviders` in `NimbleWebModule`
* `AuthServer:Authority` sets the OpenIddict issuer URI

### External Providers

| Provider               | Use Case                                      |
| ---------------------- | --------------------------------------------- |
| Azure Active Directory | Staff/admin SSO                               |
| Azure B2C              | Consumer-facing identity with custom policies |
| RapidIdentity          | SSO for education sector identity federation  |

### Brand-Specific Login Pages

Each tenant brand overrides the ABP login page via a custom `Login.cshtml.cs` at: `src/OtisEd.Nimble.Web/Branding/{Brand}/Pages/Account/Login.cshtml.cs`

Brands: `Kentucky`, `KentuckyPublic`, `NorthDakota`, `OtisEd`, `USVI`.

### Permission System

* 46+ permission groups defined in `NimblePermissions.cs`; each group follows CRUD pattern: `Default`, `Edit`, `Create`, `Delete`
* Dot-separated hierarchy: `Nimble.Reports.Edit`, `Nimble.DcForm.Approve`
* Special cross-cutting permissions: `Reports.Migrate`, `Reports.Publish`, `Reports.Configs`, `Forms.Migrate`, `Forms.Publish`, `Glossaries.Import`, `AiChatbotSetting.Edit`
* Dynamic permission store enabled: `PermissionManagementOptions.IsDynamicPermissionStoreEnabled = true`
* Impersonation enabled for both tenant and user contexts

### API Authentication (React Frontend)

* The React SPA authenticates API calls via the XSRF-TOKEN cookie: every Axios request sets the `RequestVerificationToken` header
* All requests set `withCredentials: true`
* 401 responses trigger a redirect to `/Account/Login`
* Public routes are served without authentication; these carry `IsPublic = true` on the `Route` entity
* Public user identity is represented by GUID `00000000-0000-0000-0000-000000000000`

***

## Frontend Integration

### SPA Hosting

The React app is served as a static SPA via ASP.NET Core SPA middleware:

* Production: build output at `ReactApp/build` is served via `AddSpaStaticFiles(cfg => cfg.RootPath = "ReactApp/build")`
* Development: React dev server (`yarn start`) proxies API requests to the ASP.NET Core backend
* Branded builds: `create-branded-react-app.js` merges base `ReactApp/` with brand assets into `branded-react-app/`; the branded build may supersede `ReactApp/build`

### Database-Driven Routing

Routes are not hardcoded in the React app. The `Route` entity stores `{name, path, component}` tuples per tenant. The React app fetches active routes via `GET api/app/routes`, then resolves the `component` string at runtime through `OECoreComponent.tsx` — a centralized switch statement mapping 50+ string keys to React components. This enables per-tenant route configuration without code changes.

Brand-specific route overrides are supported via an `OECustomComponent` that extends or overrides the core component registry.

### Global Properties

All route components receive a `globalProperties: ICustomGlobalProperties` prop containing:

| Field                                 | Type                          | Purpose                     |
| ------------------------------------- | ----------------------------- | --------------------------- |
| `user`                                | `IUser`                       | Current user info           |
| `settings`                            | `Record<SiteSetting, string>` | Tenant site settings        |
| `pageName` / `corePageName`           | `string`                      | Current page identification |
| `breadcrumbs`                         | `IBreadcrumb[]`               | Navigation breadcrumbs      |
| `setNotification` / `setConfirmation` | functions                     | Global messaging            |
| `academicYearID`                      | `number`                      | Current academic year       |
| `menuCollapsed`                       | `boolean`                     | Menu state                  |
| `initialized`                         | `boolean`                     | Settings load state         |

### Frontend HTTP Layer

All API calls go through typed wrappers in `core/services/`:

* `GetRequest<T>` — typed response wrapper with `isInProgress`, `isError`, `response`, `result` state
* `processGetAsync()` — GET with XSRF token, session extension, error handling
* `processGetPostAsync()` — POST-as-GET for complex query inputs
* `processGetPostMultiPartAsync()` — multipart file upload with progress callback
* Session extension: `localStorage.setItem('extendsession', '1')` on each API call

### Key Frontend Libraries

| Library           | Purpose                          |
| ----------------- | -------------------------------- |
| DevExtreme        | Data grid, charts, form controls |
| Ant Design        | UI component library             |
| React Router v6   | Client-side routing              |
| Formik + Yup      | Form state and validation        |
| Axios             | HTTP client                      |
| PowerBI client    | Power BI report embedding        |
| styled-components | Component-scoped CSS             |

***

## Background Jobs

Background jobs run via **Hangfire** with SQL Server storage, configured in `NimbleWebModule`.

| Job                      | ID                               | Schedule                                        | Description                                                                                         |
| ------------------------ | -------------------------------- | ----------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| Report Filter Generation | `daily-report-filter-generation` | Configurable cron (`ReportFiltersJob:Schedule`) | Generates pre-computed report filters via `ReportFilterJobScheduler`; uses `MisfireHandling.Strict` |
| User Page Filter Rebuild | —                                | Event-driven                                    | Rebuilds user page filter sets on trigger                                                           |
| File Sync                | —                                | On-demand                                       | `SyncFileBackgroundJob` for background file processing                                              |

### Report Filter Job Configuration

| Key                            | Purpose                            |
| ------------------------------ | ---------------------------------- |
| `ReportFiltersJob:IsEnabled`   | Enable/disable the job             |
| `ReportFiltersJob:CurrentTerm` | Term used during filter generation |
| `ReportFiltersJob:Schedule`    | Cron expression                    |

***

## Cross-Cutting Infrastructure

### Auditing

ABP audit logging via `Volo.Abp.AuditLogging`. Three audit levels used across entities:

| Base Class                 | Tracks                                          |
| -------------------------- | ----------------------------------------------- |
| `FullAuditedEntity<T>`     | Created, modified, soft-deleted (most entities) |
| `AuditedEntity<T>`         | Created and modified only (no soft delete)      |
| `CreationAuditedEntity<T>` | Created only                                    |

Login page explicitly disables auditing (`[DisableAuditing]`).

### File Storage

Azure Blob Storage via `AbpBlobStoringAzureModule`. `FileManagementContainer` defines blob containers. `DcAgency` entities store per-agency Azure SAS URLs and folder paths.

### Health Checks

Custom `NimbleDatabaseCheck` tests SQL Server connectivity. Registered via `AddNimbleHealthChecks()`.

### Clock

UTC enforced globally: `AbpClockOptions.Kind = DateTimeKind.Utc`.

### CORS

Configured from `App:CorsOrigins` (comma-separated). Allows any header, any method, with credentials.

### Theme

LeptonX theme with System style and side menu layout (`LeptonXMvcLayouts.SideMenu`).

### Localization

`NimbleResource` defined in `Domain.Shared/Localization/`. `NimbleAppService` and `NimbleController` base classes set `LocalizationResource`; assembly resources are added from Domain, Domain.Shared, Application, Application.Contracts, and Web.

### Key Configuration Values

| Key                                 | Purpose                                |
| ----------------------------------- | -------------------------------------- |
| `App:CorsOrigins`                   | CORS allowed origins (comma-separated) |
| `App:SelfUrl`                       | Application self URL                   |
| `App:CertificatePassword`           | OpenIddict certificate password        |
| `App:MicroStrategySessionServer`    | MicroStrategy server URL               |
| `App:MicroStrategyPassword`         | MicroStrategy API password             |
| `App:MaxModelBindingCollectionSize` | MVC model binding limit (default 1024) |
| `App:DisablePII`                    | Controls PII visibility in logs        |
| `AuthServer:Authority`              | OpenIddict issuer URI                  |
| `AuthServer:RequireHttpsMetadata`   | HTTPS requirement for auth metadata    |


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://nimble.docs.otised.com/technical/architecture.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
