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:
- Singleton
AppSettings(PrimaryLanguageCode = "en",SecondaryLanguageCode = "ar"). - Permissions —
PermissionSeeder.GetPermissions()(~150 keys covering all 12 modules). Newly added keys get codesPERM-NNNNNNbased onMAX(existing) + 1. - Obsolete-permission purge —
obsoleteKeyslist atDatabaseSeeder.cs:38hard-deletes dropped keys plus theirRolePermissionsandUserPermissions. Currently includesaudit-assignment.execute,audit-result.write-back,report.create/update/delete/run/read/schedule,saved-view.*,system-setting.*,asset.tag.commission,health.read. - SuperAdmin role — gets every permission.
IsSystem = trueso it can't be deleted. - Default admin user:
- Email:
admin@assettracking.local - Username:
admin - Password:
Admin@123456 - Bilingual name: "System Administrator" / "مسؤول النظام"
- Has SuperAdmin role.
- Email:
AssetStatuses—Active,InMaintenance,Missing,OutOfService,Disposed,Reserved,OnLoan(with bilingual names + colors). Detailed in 02 §Assets.- 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:
- Reads
EmailProviderSettings(live — picks up admin edits without restart). IfEnabled = false, idles. - Selects
BatchSizequeued deliveries whereNextAttemptAt <= now. - Calls
IEmailSender.SendAsyncfor each (passes throughIDataProtectorto decrypt the SMTP password). - On success →
Status = Sent,ProviderMessageIdfilled. On failure → incrementAttempts,NextAttemptAt = now + backoff, markFailedonceAttempts >= MaxAttempts. - 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 |