Phases 3 & 4: Complete data access architecture migration

Phase 3 — eliminated ApplicationDbContext from all non-exempt controllers,
routing all data access through IUnitOfWork. Added IPlainRepository<T> for
the four platform entities (Announcement, BannedIp, DashboardTip, ReleaseNote)
that intentionally don't extend BaseEntity and therefore can't use the
constrained IRepository<T>. Added permanent-exception comments to the 18
controllers that legitimately retain direct DbContext access (Identity infra,
cross-tenant platform ops, bulk streaming exports).

Phase 4 — added EnforceDataAccessArchitecture() to Program.cs, a startup
gate that reflects over every Controller subclass and throws at boot if any
non-exempt controller injects ApplicationDbContext. The app cannot start with
a violation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-28 09:17:29 -04:00
parent 90bc0d965f
commit 1cb7a8ca4a
72 changed files with 9060 additions and 2323 deletions
@@ -41,14 +41,14 @@ public class UnitOfWork : IUnitOfWork
private IRepository<AiItemPrediction>? _aiItemPredictions;
// Powder Insights
private IRepository<PowderUsageLog>? _powderUsageLogs;
private IPowderUsageLogRepository? _powderUsageLogs;
// Core repositories
private ICustomerRepository? _customers;
private IJobRepository? _jobs;
private IRepository<JobDailyPriority>? _jobDailyPriorities;
private IRepository<JobItem>? _jobItems;
private IRepository<JobItemCoat>? _jobItemCoats;
private IJobItemCoatRepository? _jobItemCoats;
private IRepository<JobItemPrepService>? _jobItemPrepServices;
private IRepository<JobChangeHistory>? _jobChangeHistories;
private IRepository<JobPrepService>? _jobPrepServices;
@@ -59,13 +59,13 @@ public class UnitOfWork : IUnitOfWork
private IRepository<QuoteItemPrepService>? _quoteItemPrepServices;
private IRepository<QuoteChangeHistory>? _quoteChangeHistories;
private IRepository<InventoryItem>? _inventoryItems;
private IRepository<InventoryTransaction>? _inventoryTransactions;
private IInventoryTransactionRepository? _inventoryTransactions;
private IRepository<Equipment>? _equipment;
private IRepository<OvenCost>? _ovenCosts;
private IRepository<CompanyBlastSetup>? _blastSetups;
private IRepository<MaintenanceRecord>? _maintenanceRecords;
private IRepository<Vendor>? _vendors;
private IRepository<JobPhoto>? _jobPhotos;
private IJobPhotoRepository? _jobPhotos;
private IRepository<JobNote>? _jobNotes;
private IRepository<CustomerNote>? _customerNotes;
private IRepository<JobStatusHistory>? _jobStatusHistory;
@@ -97,13 +97,21 @@ public class UnitOfWork : IUnitOfWork
private IRepository<SubscriptionPlanConfig>? _subscriptionPlanConfigs;
// Job Templates
private IRepository<JobTemplate>? _jobTemplates;
private IJobTemplateRepository? _jobTemplates;
private IRepository<JobTemplateItem>? _jobTemplateItems;
private IRepository<JobTemplateItemCoat>? _jobTemplateItemCoats;
private IRepository<JobTemplateItemPrepService>? _jobTemplateItemPrepServices;
// Platform content
private IPlainRepository<Announcement>? _announcements;
private IPlainRepository<BannedIp>? _bannedIps;
private IPlainRepository<DashboardTip>? _dashboardTips;
private IRepository<InAppNotification>? _inAppNotifications;
private IPlainRepository<ReleaseNote>? _releaseNotes;
// Bug Reports
private IRepository<BugReport>? _bugReports;
private IRepository<BugReportAttachment>? _bugReportAttachments;
private IRepository<ContactSubmission>? _contactSubmissions;
private IRepository<ManufacturerLookupPattern>? _manufacturerLookupPatterns;
@@ -169,8 +177,8 @@ public class UnitOfWork : IUnitOfWork
// Powder Insights
/// <summary>Repository for <see cref="PowderUsageLog"/> records capturing per-coat powder consumption; used by powder-usage analytics.</summary>
public IRepository<PowderUsageLog> PowderUsageLogs =>
_powderUsageLogs ??= new Repository<PowderUsageLog>(_context);
public IPowderUsageLogRepository PowderUsageLogs =>
_powderUsageLogs ??= new PowderUsageLogRepository(_context);
// Core repositories
/// <summary>Repository for <see cref="Customer"/> records (commercial and non-commercial); tenant-filtered with soft delete.</summary>
@@ -190,8 +198,8 @@ public class UnitOfWork : IUnitOfWork
_jobItems ??= new Repository<JobItem>(_context);
/// <summary>Repository for <see cref="JobItemCoat"/> powder coat passes; tenant-filtered with soft delete.</summary>
public IRepository<JobItemCoat> JobItemCoats =>
_jobItemCoats ??= new Repository<JobItemCoat>(_context);
public IJobItemCoatRepository JobItemCoats =>
_jobItemCoats ??= new JobItemCoatRepository(_context);
public IRepository<JobItemPrepService> JobItemPrepServices =>
_jobItemPrepServices ??= new Repository<JobItemPrepService>(_context);
@@ -232,8 +240,8 @@ public class UnitOfWork : IUnitOfWork
_inventoryItems ??= new Repository<InventoryItem>(_context);
/// <summary>Repository for <see cref="InventoryTransaction"/> stock movements; tenant-filtered with soft delete.</summary>
public IRepository<InventoryTransaction> InventoryTransactions =>
_inventoryTransactions ??= new Repository<InventoryTransaction>(_context);
public IInventoryTransactionRepository InventoryTransactions =>
_inventoryTransactions ??= new InventoryTransactionRepository(_context);
/// <summary>Repository for <see cref="Equipment"/> records (ovens, sandblasters, booths); tenant-filtered with soft delete.</summary>
public IRepository<Equipment> Equipment =>
@@ -256,8 +264,8 @@ public class UnitOfWork : IUnitOfWork
_vendors ??= new Repository<Vendor>(_context);
/// <summary>Repository for <see cref="JobPhoto"/> attachments; tenant-filtered with soft delete.</summary>
public IRepository<JobPhoto> JobPhotos =>
_jobPhotos ??= new Repository<JobPhoto>(_context);
public IJobPhotoRepository JobPhotos =>
_jobPhotos ??= new JobPhotoRepository(_context);
/// <summary>Repository for <see cref="JobNote"/> free-text staff notes on jobs; tenant-filtered with soft delete.</summary>
public IRepository<JobNote> JobNotes =>
@@ -372,11 +380,35 @@ public class UnitOfWork : IUnitOfWork
public IRepository<SubscriptionPlanConfig> SubscriptionPlanConfigs =>
_subscriptionPlanConfigs ??= new Repository<SubscriptionPlanConfig>(_context);
// Platform content
/// <summary>Repository for <see cref="Announcement"/> platform-wide announcements; no tenant filter, no soft delete.</summary>
public IPlainRepository<Announcement> Announcements =>
_announcements ??= new PlainRepository<Announcement>(_context);
/// <summary>Repository for <see cref="BannedIp"/> IP ban records; no tenant filter, no soft delete.</summary>
public IPlainRepository<BannedIp> BannedIps =>
_bannedIps ??= new PlainRepository<BannedIp>(_context);
/// <summary>Repository for <see cref="DashboardTip"/> rotating tip-of-the-day entries; no tenant filter, no soft delete.</summary>
public IPlainRepository<DashboardTip> DashboardTips =>
_dashboardTips ??= new PlainRepository<DashboardTip>(_context);
/// <summary>Repository for <see cref="InAppNotification"/> bell-notification records; tenant-filtered with soft delete.</summary>
public IRepository<InAppNotification> InAppNotifications =>
_inAppNotifications ??= new Repository<InAppNotification>(_context);
/// <summary>Repository for <see cref="ReleaseNote"/> platform changelog entries; no tenant filter, no soft delete.</summary>
public IPlainRepository<ReleaseNote> ReleaseNotes =>
_releaseNotes ??= new PlainRepository<ReleaseNote>(_context);
// Bug Reports
/// <summary>Repository for <see cref="BugReport"/> user-submitted bug reports; tenant-filtered with soft delete.</summary>
public IRepository<BugReport> BugReports =>
_bugReports ??= new Repository<BugReport>(_context);
public IRepository<BugReportAttachment> BugReportAttachments =>
_bugReportAttachments ??= new Repository<BugReportAttachment>(_context);
// Contact Us
/// <summary>Repository for <see cref="ContactSubmission"/> contact form submissions; platform admins see all, company users see their own.</summary>
public IRepository<ContactSubmission> ContactSubmissions =>
@@ -397,8 +429,8 @@ public class UnitOfWork : IUnitOfWork
// Job Templates
/// <summary>Repository for <see cref="JobTemplate"/> reusable job blueprints; tenant-filtered with soft delete.</summary>
public IRepository<JobTemplate> JobTemplates =>
_jobTemplates ??= new Repository<JobTemplate>(_context);
public IJobTemplateRepository JobTemplates =>
_jobTemplates ??= new JobTemplateRepository(_context);
/// <summary>Repository for <see cref="JobTemplateItem"/> item definitions within a job template.</summary>
public IRepository<JobTemplateItem> JobTemplateItems =>