06 — Operations

Build, run, configure, seed, deploy, and monitor.

Prerequisites

Tool Version Notes
.NET SDK 8.0.x <TargetFramework>net8.0</TargetFramework>
Node.js 18.x or 20.x LTS Angular 18 supports both
npm 9+ Or yarn/pnpm — package-lock is npm
SQL Server 2019+ LocalDB / Express / Developer all work
dotnet ef 8.x dotnet tool install --global dotnet-ef --version 8.0.11 if missing
gcloud auth login etc. only needed for deployment to GCP/Azure depending on target

The repository's dotnet-tools.json may pin local tool versions — restore with dotnet tool restore from the repo root.

Build & test

Backend

Run from the repository root:

dotnet restore
dotnet build                 # builds the entire solution
dotnet test                  # runs all three test projects
dotnet test --filter "FullyQualifiedName~ClassName.MethodName"   # single test
dotnet run --project src/AssetTracking.API                       # starts the API

The API listens on the URLs configured in Properties/launchSettings.json (defaults to https://localhost:5001 + http://localhost:5000). Swagger UI is auto-mounted at /swagger in Development.

Frontend

Run from frontend/:

cd frontend
npm install
npm start                    # ng serve at http://localhost:4200; proxies /api → backend
npm run build                # production build to dist/frontend
npm run watch                # ng build --watch --configuration development
npm test                     # karma + jasmine

proxy.conf.json proxies /api/* to the backend so the SPA never sees a CORS request in development.

Configuration

Backend — src/AssetTracking.API/appsettings.json

{
  "ConnectionStrings": {
    "SqlServer": "Server=.;Database=AssetTracking;Trusted_Connection=True;TrustServerCertificate=True;MultipleActiveResultSets=true"
  },
  "JwtSettings": {
    "Secret": "YourSuperSecretKeyThatIsAtLeast32CharactersLong!!",
    "Issuer": "AssetTracking",
    "Audience": "AssetTrackingClients",
    "AccessTokenExpirationMinutes": 15,
    "RefreshTokenExpirationDays": 30
  },
  "Serilog": {
    "MinimumLevel": {
      "Default": "Information",
      "Override": { "Microsoft": "Warning", "Microsoft.EntityFrameworkCore": "Warning", "System": "Warning" }
    }
  },
  "Cors": { "AllowedOrigins": [ "http://localhost:4200" ] },
  "FileStorage": { "RootPath": "AppData/Documents" },
  "Seeding": {
    "HierarchyConfigs": true,
    "WorkflowRoles": true,
    "DemoUsers": true
  },
  "AllowedHosts": "*"
}
Section Purpose
ConnectionStrings:SqlServer EF Core connection string (any SQL Server provider; PostgreSQL would require swapping UseSqlServer in Persistence/DependencyInjection.cs)
JwtSettings.Secret MUST be ≥ 32 chars — HMAC-SHA256 signing key. Override in production via env var or User Secrets.
JwtSettings.AccessTokenExpirationMinutes Access token lifetime (default 15)
JwtSettings.RefreshTokenExpirationDays Refresh token lifetime (default 30)
Serilog.MinimumLevel Serilog's standard config; logs to console by default
Cors:AllowedOrigins List of origins allowed; Angular dev server is http://localhost:4200
FileStorage.RootPath Where LocalFileStorage writes documents/photos. Path is relative to the API's content root unless absolute. Make sure the process can write here.
Seeding.* Optional demo-data flags (see §Seeding)

Production overrides

Standard ASP.NET Core config layering applies — every key can be overridden by:

  • appsettings.Production.json
  • Environment variables (e.g., JwtSettings__Secret, ConnectionStrings__SqlServer)
  • dotnet user-secrets (development only)
  • IConfiguration providers wired in Program.cs

For container deployments, prefer environment variables. Never check secrets into appsettings.json.

Frontend — src/environments/environment.ts

export const environment = {
  production: false,
  apiBaseUrl: '/api/v1'
};

apiBaseUrl: '/api/v1' works because proxy.conf.json reroutes /api/* to the backend. In production the SPA is typically served by the same host as the API, so the relative path keeps working.

For deployments where the SPA is on a different origin, override at build time via environment.prod.ts:

export const environment = {
  production: true,
  apiBaseUrl: 'https://api.example.com/api/v1'
};

Database setup

First run

# 1. Create the database from migrations (or let the API do it on first boot —
#    Program.cs:112 calls MigrateAsync())
dotnet ef database update --project src/AssetTracking.Persistence --startup-project src/AssetTracking.API

# 2. Run the API. On first boot it:
#    - Applies any pending migrations (idempotent)
#    - Runs DatabaseSeeder.SeedAsync (idempotent)
#    - Drops sample import .xlsx files into AppData/SampleData/ (only if folder is empty)
dotnet run --project src/AssetTracking.API

Adding a migration

dotnet ef migrations add MyMigrationName \
    --project src/AssetTracking.Persistence \
    --startup-project src/AssetTracking.API

Migration files land in src/AssetTracking.Persistence/Migrations/. They're forward-only; never edit a migration that's already shipped.

The API project must build cleanly for dotnet ef migrations add to work. If the API is running, the build will fail with file-lock errors on the API's DLLs — stop the running process first.

Reverting / removing an unapplied migration

# Remove the most recent (still-pending) migration
dotnet ef migrations remove --project src/AssetTracking.Persistence --startup-project src/AssetTracking.API

# Roll back to a specific migration (then 'remove' as needed)
dotnet ef database update <PreviousMigrationName> --project src/AssetTracking.Persistence --startup-project src/AssetTracking.API

Manual SQL helpers

src/AssetTracking.Persistence/Migrations/Manual_SoftDeleteOrphanedAssetChildren.sql — operator-run script that soft-deletes orphan rows after a master-data delete. Idempotent. Apply via sqlcmd or your DB tool of choice.

Seeding

DatabaseSeeder.SeedAsync runs at every boot in Program.cs:116. It's idempotent — already-seeded rows are skipped.

Always seeded

These are load-bearing:

  1. Singleton AppSettings (PrimaryLanguageCode = "en", SecondaryLanguageCode = "ar").
  2. PermissionsPermissionSeeder.GetPermissions() (~150 keys covering all 12 modules). Newly added keys get codes PERM-NNNNNN based on MAX(existing) + 1.
  3. Obsolete-permission purgeobsoleteKeys list at DatabaseSeeder.cs:38 hard-deletes dropped keys plus their RolePermissions and UserPermissions. Currently includes audit-assignment.execute, audit-result.write-back, report.create/update/delete/run/read/schedule, saved-view.*, system-setting.*, asset.tag.commission, health.read.
  4. SuperAdmin role — gets every permission. IsSystem = true so it can't be deleted.
  5. Default admin user:
    • Email: admin@assettracking.local
    • Username: admin
    • Password: Admin@123456
    • Bilingual name: "System Administrator" / "مسؤول النظام"
    • Has SuperAdmin role.
  6. AssetStatusesActive, InMaintenance, Missing, OutOfService, Disposed, Reserved, OnLoan (with bilingual names + colors). Detailed in 02 §Assets.
  7. Notification templates — every (Key, Channel) from the catalog. Bilingual subject + body. The seeder upserts: existing templates get their bodies refreshed if blank.

Optional flags — Seeding section

Flag Effect when true
HierarchyConfigs Seeds default Org/Loc/Class hierarchy levels (e.g., Building → Floor → Room for Location). Match the sample Excel workbooks.
WorkflowRoles Seeds 11 built-in roles. Each is IsBuiltIn = true, so on every startup the seeder upserts new permissions added to its allowlist.
DemoUsers Seeds 11 demo users (one per workflow role + Mobile auditor) with shared password Demo@123456.

Built-in workflow roles + demo users (when both flags set)

Role Demo email / username Permissions (high-level)
Transfer Requester requester@assettracking.local / requester Common reads + asset-transfer.read, .create, .submit, .cancel
Transfer Approver approver@assettracking.local / approver Common reads + asset-transfer.read, .approve, .reject
Transfer Receiver receiver@assettracking.local / receiver Common reads + asset-transfer.read, .receive, .complete
Audit Planner planner@assettracking.local / planner + user.read, audit-plan.*, audit-assignment.read
Auditor auditor@assettracking.local / auditor + audit-assignment.read, .submit, audit-result.read
Audit Reviewer reviewer@assettracking.local / reviewer + audit-result.read, .review
Mobile Auditor mobile@assettracking.local / mobile Same as Auditor; UserType=Mobile (lands in /mobile shell)
Checkout Issuer issuer@assettracking.local / issuer + check-out.create
Checkout Returner returner@assettracking.local / returner + check-out.return, check-out.cancel
Asset Custodian custodian@assettracking.local / custodian Asset-read + assignment-read; the "I have assets" persona

Demo users + workflow roles are additive on every boot: the seeder grants any newly-listed permission to the existing role and busts the affected users' permission cache. See 02 §Identity.

Disabling seed for production

"Seeding": { "HierarchyConfigs": false, "WorkflowRoles": false, "DemoUsers": false }

The "always seeded" items still run; only the optional flags are gated. Production deployments typically keep HierarchyConfigs: true (you want the configured tree depths) and the others false.

Default credentials (development)

User Password Notes
admin@assettracking.local Admin@123456 SuperAdmin — every permission
<workflow-role>@assettracking.local Demo@123456 When Seeding.DemoUsers = true

Change these before any production deployment. Use the admin UI to rotate the admin password (/profile) and disable / delete demo users.

Background processes

NotificationDeliveryWorker (Infrastructure/Notifications/NotificationDeliveryWorker.cs)

Hosted service. Drains the NotificationDeliveries queue:

  1. Reads EmailProviderSettings (live — picks up admin edits without restart). If Enabled = false, idles.
  2. Selects BatchSize queued deliveries where NextAttemptAt <= now.
  3. Calls IEmailSender.SendAsync for each (passes through IDataProtector to decrypt the SMTP password).
  4. On success → Status = Sent, ProviderMessageId filled. On failure → increment Attempts, NextAttemptAt = now + backoff, mark Failed once Attempts >= MaxAttempts.
  5. Sleeps PollIntervalSeconds (default 15 s) before next tick.

Tunable from EmailProviderSettings: BatchSize, PollIntervalSeconds, MaxAttempts, RedirectAllEnabled + RedirectAllToAddress (staging escape hatch).

SampleDataGenerator (Infrastructure/Services/SampleDataGenerator.cs)

Singleton, called from Program.cs after seeding:

sampleDataGenerator.WriteIfMissing(app.Environment.ContentRootPath);

If AppData/SampleData/ is empty, drops importable .xlsx files for Organizations, Locations, Classifications, Vendors, Manufacturers, Assets. Skipped if the folder already has files — lets devs regenerate by deleting the folder.

Observability

Structured logs — Serilog

Configured in Program.cs:17–23:

builder.Host.UseSerilog((context, config) => config
    .ReadFrom.Configuration(context.Configuration)
    .Enrich.FromLogContext()
    .Enrich.WithMachineName()
    .Enrich.WithThreadId()
    .WriteTo.Console());

Logs go to the console.

Every log line carries a CorrelationId property (pushed by CorrelationIdMiddleware), so a single web request's trail can be filtered with CorrelationId = "<guid>".

Request log table — RequestLogs

RequestLoggingMiddleware writes one row per request (skipping /health and /swagger). Fields documented in 03. Useful for ad-hoc analytics; retention is operator-driven (no built-in cleaner).

Audit log table — AuditLogs

AuditLogInterceptor writes a row for every change to allow-listed entity types (Created/Updated/Deleted with JSON before/after + diff). Fields documented in 03. Retention: 7 years.

Login audit table — LoginAudits

LoginCommandHandler writes one row per login attempt (success or fail). Useful for security review. No automatic retention.

Retention recommendations

Recommended retention by table:

Table Retention Cleanup
RequestLogs 90 days Operator-scheduled SQL job
AuditLogs 7 years (or per regulation) Operator-scheduled
Notifications 180 days after IsRead = true Operator-scheduled
LoginAudits Per security policy Operator-scheduled
RefreshTokens Cleared by rotation; revoked rows retained for forensic analysis Operator-scheduled hard-delete after refresh-token TTL × N

There is no built-in retention job. Wire one as a BackgroundService, a scheduled SQL Agent job, or a separate cron container — whichever fits the deployment topology.

Deployment topology

The system is a single-process .NET 8 API + an Angular SPA, backed by SQL Server and a local filesystem for documents.

flowchart LR
  Browser -->|HTTPS| LB[Reverse proxy / nginx / IIS]
  LB --> API[ASP.NET Core 8 API<br/>net8.0]
  API --> DB[(SQL Server)]
  API --> FS[(File storage<br/>AppData/Documents)]
  API --> SMTP[(SMTP relay)]
Layer Hosting option
API + notification worker dotnet run behind nginx / IIS, OR Azure App Service / AWS ECS
Database Managed SQL Server (Azure SQL DB / RDS) or self-managed
File storage Local volume mount; back it up
SMTP Any provider (Gmail App Password, SendGrid, Office 365, etc.) — configured at /settings/email at runtime

CI/CD

A typical pipeline:

1. dotnet restore
2. dotnet build --no-restore --configuration Release
3. dotnet test --no-build --configuration Release
4. cd frontend && npm ci
5. npm run build -- --configuration production
6. dotnet publish (or container build)
7. Deploy

The build-time RequirePermission coverage test (tests/AssetTracking.API.Tests/Authorization/...) is the single most important gate: it scans every controller action via reflection and fails the build if any lacks [RequirePermission] or [AllowAnonymous]. Don't skip tests in CI.

Backups & disaster recovery

Asset Backup
Database Standard SQL Server backups (full + differential + log). Test restores monthly.
AppData/Documents Filesystem snapshot or rsync. Document SHA-256s let you detect corruption.
EmailProviderSettings.PasswordEncrypted Encrypted via ASP.NET Core Data Protection. The data-protection key ring lives at the OS-default location (%LOCALAPPDATA%\\ASP.NET\\DataProtection-Keys on Windows). Back this up too — without it, the encrypted password can't be decrypted on a restored host.

Where to go next

For… See
Endpoint URLs & request/response shapes 07-api-reference
Module business rules + handler details 02-backend-modules
Schema 03-database