ShopFloor - Architecture
Architecture
Section titled “Architecture”How the code is organized, how it’s wired up at runtime, and how requests flow through it. For a higher-level system view see 01-system-overview.md. For the data shape see 11-data-model.md.
⚠️ Assumption markers indicate items not fully verified — flag corrections welcome.
1. Project layout (Clean Architecture)
Section titled “1. Project layout (Clean Architecture)”The solution applies Clean Architecture with strict unidirectional dependencies. There are no upward references — Core never knows about Infrastructure, Infrastructure never knows about Presentation.
flowchart TD subgraph Presentation WAPP[Presentation.WebAPP<br/>MVC + Razor] WAPI[Presentation.WebAPI<br/>Internal API] EXT[Presentation.ExternalAPI<br/>Partner API] end subgraph Services WASI[Services.WebAppServicesImplementation<br/>~150 controller-services] WASC[Services.WebAppServicesContracts<br/>Interfaces] EMAIL[Services.EmailService] HP[Services.HPPCBApi] LOG[Services.LoggerService] end subgraph Infrastructure PERSIST[Infrastructure.Persistance<br/>EF Core, DbContexts, repos, migrations] end subgraph Core DOMAIN[Core.Domain<br/>Entities + enums] CONTRACTS[Core.Contracts<br/>Repository + service interfaces] SHARED[Core.Shared<br/>DTOs, constants] end WAPP --> WASI --> WASC --> PERSIST --> DOMAIN WASI --> CONTRACTS WASI --> SHARED WASI --> EMAIL WASI --> HP WASI --> LOG WAPI --> WASI EXT --> WASIPresentation layer
Section titled “Presentation layer”| Project | Purpose | Auth |
|---|---|---|
Presentation.WebAPP | Main MVC application — Razor views with Syncfusion EJ2 components. Hosts Hangfire dashboard. | ASP.NET Identity cookies (+ JWT + API Key) |
Presentation.WebAPI | Internal admin/integration API (BOM, DPK, Imaging, OEM, Projects, Tenants, Users). | API Key only |
Presentation.ExternalAPI | Partner-facing API for pushing inbound orders. | API Key + Bearer token |
Services layer
Section titled “Services layer”Services.WebAppServicesImplementation is where the business logic lives. ~150 “controller-services” — each one is a single domain capability (e.g., DockStationControllerService, RMAControllerService, PurchaseOrderControllerService). Controllers in the Presentation layer are thin delegates that hand off to these.
Three small cross-cutting service projects sit alongside:
Services.EmailService— SMTP wrapper.Services.HPPCBApi— typed client for HP’s Parts Content Base.Services.LoggerService— Serilog integration helpers.
Infrastructure layer
Section titled “Infrastructure layer”Infrastructure.Persistance owns EF Core. Holds three DbContexts, ~100 repositories, and all migrations.
Core layer
Section titled “Core layer”Three projects, deliberately small:
Core.Domain— ~225 entity classes, enums, identity model.Core.Contracts— interfaces only (IRepositoryBase<T>,IRepositoryManager, etc.).Core.Shared— DTOs, constants, shared helpers.
2. Repository + Unit-of-Work pattern
Section titled “2. Repository + Unit-of-Work pattern”Every entity has a repository implementing IRepositoryBase<T> (CRUD + FindByCondition). Repositories are aggregated into a RepositoryManager that:
- Holds the
RepositoryDbContext. - Lazily instantiates each repository on first access.
- Exposes
Save()andSaveAsync()as the unit-of-work commit.
// Inside a controller-servicevar workTracking = await _repositoryManager.WorkTracking.FindByConditionAsync(...);workTracking.StationID = newStation.StationID;await _repositoryManager.SaveAsync();A separate TenantRepositoryManager does the same for TenantDbContext.
🔧 Refactor finding: the
RepositoryBase<T>generic is repeated across the codebase (one variant forAuditLogDbContext, another for the main context). The duplication should be collapsed once contexts are confirmed to converge. See 20-refactor-findings.md.
3. ServiceManager facade
Section titled “3. ServiceManager facade”Mirror of RepositoryManager for services. ServiceManager exposes 100+ lazy-loaded controller-service properties:
public class ServiceManager : IServiceManager{ public Lazy<IDockStationControllerService> DockStationControllerService { get; } public Lazy<IRMAControllerService> RMAControllerService { get; } // ... ~100 more}Controllers and other services consume one IServiceManager instead of a 100-parameter constructor.
🔧 Refactor finding: 100+ properties in one file is cognitively heavy. Domain grouping (e.g.,
ServiceManager.Repair,ServiceManager.Sales) would help. See 20-refactor-findings.md.
4. Three DbContexts
Section titled “4. Three DbContexts”| Context | DB engine | Purpose | Tracking | Migrations table |
|---|---|---|---|---|
RepositoryDbContext | SQL Server | Main application data — ~120 DbSet<> properties. Inherits MultiTenantIdentityDbContext so it gets Finbuckle’s global query filter scoping rows by TenantId. | Tracking | __EFMigrationsHistory |
TenantDbContext | SQL Server | Tenant registry — a single DbSet<CTLShopFloorTenantInfo>. Auto-stamps CreatedAt / LastModified (lines 27–57). | Tracking | __EFMigrationsHistoryTenants |
FishbowlDbContext | MySQL (Pomelo) | Read-only mirror of Fishbowl ERP — ~60 DbSets. ChangeTracker.QueryTrackingBehavior = NoTracking globally (line 17). | No-tracking | n/a (no migrations) |
Migration gating
Section titled “Migration gating”Program.cs lines 79–100:
- Read
Migrations:allowMigrationsflag. - If
false: callHasPendingModelChanges()on each context; throw on mismatch. This forces explicit migration management when running against production. - If
true: apply pending migrations toTenantDbContext, thenRepositoryDbContext.
This is the dev/prod safety mechanism described in CLAUDE.md. Same idea governs Hangfire via JOBS:allowJobsExecution.
Tenant query filter
Section titled “Tenant query filter”RepositoryDbContext inherits MultiTenantIdentityDbContext<AppUser, AppRole, ...> which automatically injects WHERE TenantId = @currentTenant on every query for entities that carry TenantId. Developers don’t manually filter — Finbuckle does it.
⚠️ Assumption: no entity opts out of this filter, but
IgnoreQueryFilters()overrides exist somewhere in the code (likely in cross-tenant admin operations). Worth a grep before refactor.
5. Multi-tenant configuration
Section titled “5. Multi-tenant configuration”Finbuckle is configured in ServiceExtensions.ConfigureMultiTenant (lines 336–343):
.WithHeaderStrategy("tenant") // 1. HTTP header.WithSessionStrategy() // 2. Session.WithRouteStrategy("_tenant_") // 3. Route parameter.WithEFCoreStore<TenantDbContext, CTLShopFloorTenantInfo>()Resolution is in priority order top-to-bottom. The middleware that does the resolution is registered in Program.cs line 153.
The ExternalAPI project uses a custom ExternalApiMultiTenantStrategy (which likely reads the tenant from the partner’s API key or Bearer token claims — ⚠️ confirm by grep).
6. Request lifecycle (middleware order)
Section titled “6. Request lifecycle (middleware order)”The middleware pipeline in Program.cs lines 102–172, in order:
flowchart TD A[Request arrives] --> B[Swagger UI<br/>dev only] B --> C[HTTP redirection] C --> D[Static files] D --> E[Correlation-ID middleware<br/>injects Guid into Serilog LogContext] E --> F[404 → /Home/NotFound] F --> G[Session] G --> H[Finbuckle MultiTenant resolver] H --> I[Serilog request logging<br/>LogEnricher.EnrichFromRequest] I --> J[Authentication] J --> K[Authorization] K --> L[Custom ExceptionMiddleware] L --> M[Hangfire dashboard<br/>if jobs enabled] M --> N[MVC routing → Controller] N --> O[ServiceManager.X.Method] O --> P[RepositoryManager.Y.Op] P --> Q[(SQL Server)] P --> R[Audit.NET → MongoDB]Custom middleware
Section titled “Custom middleware”CorrelationIdMiddleware(lines 109–121) — generates aGuidper request and pushes it intoLogContextso every Serilog event in the request carries the same correlation ID.ExceptionMiddleware(line 145) — catchesDbUpdateException,NullSyncFusionObjectException, and others; translates to a JSON error envelope.SlowQueryInterceptor(line 156 ofServiceExtensions) — registered onRepositoryDbContext; logs any EF query that exceedsSlowQueryThreshold(default 5000ms).
7. Authentication & authorization
Section titled “7. Authentication & authorization”Authentication
Section titled “Authentication”Three concurrent schemes are registered:
| Scheme | Issued via | Used by | Lifetime |
|---|---|---|---|
| Cookies (default) | Login form on WebAPP | Interactive users | 1 hour sliding |
| JWT Bearer | LoginControllerService via AppSettings:Token shared secret | API consumers, mobile clients ⚠️ | Token-dependent |
| API Key | ApiKeyAuthenticationHandler (custom) | ExternalAPI partner calls, WebAPI internal | Per-key |
Identity uses a custom AppUser : IdentityUser<Guid> and AppRole : IdentityRole<Guid> (Guid PK override). Password policy is intentionally loose — no uppercase / digit / special-char requirements.
🔧 Refactor finding: loose password policy combined with secrets in
appsettings.json(see §10) is the biggest security smell. Theappsettings.jsonfile is committed with live-looking connection strings, API keys (Fishbowl, Mercado Libre, SendGrid, Seq), Azure credentials, and the JWT signing secret. This must be moved to a secrets manager before any new production deploy.
Authorization
Section titled “Authorization”A custom ResourceAccessRequirement policy (and ResourceAccessHandler):
- Reads the current
__project__Guid from session. - Looks up the user’s roles in that project context.
- Builds claims dynamically from role permissions.
- Calls into
vwRolePermissionsRepository(a DB view) to decide whether the requested action / resource is allowed.
🔧 Refactor finding: the handler reaches deeply into
context.Resource → RouteEndpointwith no exception handling — fragile.
8. Dependency injection setup
Section titled “8. Dependency injection setup”DI registration happens via extension methods over IServiceCollection, called from Program.cs. Most go through ServiceExtensions in WebAPP.
Scoped (per-request)
Section titled “Scoped (per-request)”IRepositoryManager→RepositoryManagerITenantRepositoryManager→TenantRepositoryManagerIServiceManager→ServiceManagerIEmailSenderIPalletServiceFactory,BoxPalletControllerService- All Fishbowl / Mercado Libre / Reporting domain services
- Seven
IParameterDataSourceimplementations (resolved asIEnumerable<IParameterDataSource>)
Singleton (app lifetime)
Section titled “Singleton (app lifetime)”ILoggerManagerICacheService→MemoryCacheServiceIConfiguration,IHttpContextAccessor
⚠️ Assumption: the seven
IParameterDataSourceimplementations are all actually used. Worth confirming — if any are dead, register only the live ones.
9. Cross-cutting concerns
Section titled “9. Cross-cutting concerns”Logging — Serilog
Section titled “Logging — Serilog”Configured in appsettings.json (lines 48–93). Three sinks active:
- Console — local dev.
- MSSqlServer —
ErrorLogtable, errors only. - Seq — central aggregation at the URL configured.
Enrichers: LogContext, MachineName, EnvironmentName, ExceptionDetails, ProcessId, ThreadId, plus custom request enrichment via LogEnricher.EnrichFromRequest. CorrelationId is added by the correlation middleware so every log line for a request shares the ID.
Minimum level: Verbose (all levels captured).
Auditing — Audit.NET → MongoDB
Section titled “Auditing — Audit.NET → MongoDB”Configured in ServiceExtensions.ConfigureAuditTrail (lines 272–321). EF Core interceptor captures entity changes:
- Database:
auditLog - Collection:
shopFloor - Ignored entities:
AuditLog(itself),SalesImport - Ignored fields on
WorkTracking:SystemInformationBlob,WindowsTestResultBlob(too large; stored separately) - User attribution: from
HttpContext(line 276); defaults to"System"for background jobs
The legacy SQL AuditLog table still exists, and the PurgeAuditLogJob is disabled (commented out) because audit rows now write directly to Mongo (PurgeAuditLogJob line 47 comment). ⚠️ Possible smell: SQL AuditLog may still receive rows in some paths — confirm before assuming Mongo is the sole sink.
Caching — Memory + Distributed
Section titled “Caching — Memory + Distributed”- In-process:
ICacheService→MemoryCacheService. - Distributed session: SQL Server
SessionCachetable (Identity data-protection keys also stored, in Azure Blobblobstorageconnection).
Background jobs — Hangfire
Section titled “Background jobs — Hangfire”- Storage: SQL Server.
- Dashboard at
/hangfire(authenticated, only whenJOBS:allowJobsExecution=true). - ~18 recurring jobs registered via
ConfigureBackgroundJobs. See 14-jobs-and-integrations.md.
10. Configuration files
Section titled “10. Configuration files”| File | Tracked in git? | Purpose |
|---|---|---|
appsettings.json | yes | Defaults; contains secrets ⚠️ |
appsettings.Development.json | yes | Dev overrides |
appsettings.Staging.json | yes | Staging overrides |
appsettings.Production.json | yes | Production overrides |
*.Local.json | no (gitignored) | Per-developer local overrides — connection strings, secrets |
DbMigrationSettings.Local.json | yes (suggested) | Per readme.md, the one .Local.json that should be committed, defaulting allowMigrations=false for safety |
Two critical config flags:
| Flag | Default | Effect |
|---|---|---|
Migrations:allowMigrations | false | When false, throws on startup if model has pending migrations. Prevents accidentally migrating prod from a dev machine. |
JOBS:allowJobsExecution | false | When false, no Hangfire jobs are registered. Prevents dev machines from running production jobs against prod data. |
11. Architectural smells (cross-referenced to refactor findings)
Section titled “11. Architectural smells (cross-referenced to refactor findings)”| Smell | Severity | Detail |
|---|---|---|
Secrets committed in appsettings.json | 🔴 Critical | DB passwords, API keys, JWT secret, Azure creds. Must move to KeyVault / user-secrets before next prod cycle. |
| Hardcoded tenant names in business logic | 🟠 High | Triage routing, sales-import facility mapping, HP COA job all branch on string-equal tenant name. |
ServiceManager with 100+ Lazy properties | 🟡 Medium | Hard to navigate; consider domain grouping. |
Duplicate RepositoryBase<T> generics | 🟡 Medium | One for AuditLogDbContext, one for the main context. Collapse if possible. |
| Loose Identity password policy | 🟡 Medium | No complexity requirements. |
ResourceAccessHandler fragility | 🟡 Medium | No exception handling around context.Resource → RouteEndpoint. |
| Migrations flag uses string compare | 🟢 Low | Equals("true") vs GetValue<bool>() inconsistency. |
| Per-context query-tracking is implicit | 🟢 Low | FishbowlDbContext is no-tracking globally; non-obvious to a new dev. |
Full running list in 20-refactor-findings.md.
12. Where to read next
Section titled “12. Where to read next”- For what the entities and their relationships look like → 11-data-model.md
- For how routing/scanning actually executes → 13-workflow-engine.md
- For what runs in the background and external integrations → 14-jobs-and-integrations.md