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
@@ -9,7 +9,6 @@ using PowderCoating.Application.Services;
using PowderCoating.Core.Entities;
using PowderCoating.Core.Enums;
using PowderCoating.Core.Interfaces;
using PowderCoating.Infrastructure.Data;
using PowderCoating.Shared.Constants;
using PowderCoating.Web.Hubs;
@@ -19,7 +18,6 @@ namespace PowderCoating.Web.Controllers;
public class JobsPriorityController : Controller
{
private readonly IUnitOfWork _unitOfWork;
private readonly ApplicationDbContext _context;
private readonly ILogger<JobsPriorityController> _logger;
private readonly UserManager<ApplicationUser> _userManager;
private readonly ITenantContext _tenantContext;
@@ -27,14 +25,12 @@ public class JobsPriorityController : Controller
public JobsPriorityController(
IUnitOfWork unitOfWork,
ApplicationDbContext context,
ILogger<JobsPriorityController> logger,
UserManager<ApplicationUser> userManager,
ITenantContext tenantContext,
IHubContext<ShopHub> shopHub)
{
_unitOfWork = unitOfWork;
_context = context;
_logger = logger;
_userManager = userManager;
_tenantContext = tenantContext;
@@ -63,13 +59,7 @@ public class JobsPriorityController : Controller
var today = date?.Date ?? DateTime.Today;
// Get all jobs scheduled for today with related data
var jobs = await _context.Jobs
.Include(j => j.Customer)
.Include(j => j.JobStatus)
.Include(j => j.JobPriority)
.Include(j => j.AssignedUser)
.Where(j => j.ScheduledDate.HasValue && j.ScheduledDate.Value.Date == today && !j.IsDeleted)
.ToListAsync();
var jobs = await _unitOfWork.Jobs.GetScheduledJobsForDateAsync(today);
// Get existing priority records for today
var existingPriorities = await _unitOfWork.JobDailyPriorities
@@ -108,15 +98,14 @@ public class JobsPriorityController : Controller
.ToListAsync();
// Get maintenance records scheduled for today (Scheduled or InProgress)
var maintenanceItems = await _context.MaintenanceRecords
.Include(m => m.Equipment)
.Include(m => m.AssignedUser)
.Where(m => m.ScheduledDate.Date == today && !m.IsDeleted &&
(m.Status == MaintenanceStatus.Scheduled ||
m.Status == MaintenanceStatus.InProgress))
var maintenanceItems = (await _unitOfWork.MaintenanceRecords.FindAsync(
m => m.ScheduledDate.Date == today &&
(m.Status == MaintenanceStatus.Scheduled || m.Status == MaintenanceStatus.InProgress),
false,
m => m.Equipment, m => m.AssignedUser))
.OrderByDescending(m => (int)m.Priority)
.ThenBy(m => m.ScheduledDate)
.ToListAsync();
.ToList();
ViewBag.ScheduledDate = today;
ViewBag.MaintenanceItems = maintenanceItems;
@@ -378,14 +367,10 @@ public class JobsPriorityController : Controller
{
try
{
var record = await _context.MaintenanceRecords.FindAsync(maintenanceId);
var record = await _unitOfWork.MaintenanceRecords.GetByIdAsync(maintenanceId);
if (record == null || record.IsDeleted)
return Json(new { success = false, message = "Maintenance record not found" });
// FindAsync bypasses global query filters — verify company ownership explicitly
if (!_tenantContext.IsSuperAdmin() && record.CompanyId != _tenantContext.GetCurrentCompanyId())
return Json(new { success = false, message = "Access denied." });
string workerName = "Unassigned";
if (!string.IsNullOrEmpty(workerId))
{
@@ -402,7 +387,7 @@ public class JobsPriorityController : Controller
}
record.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
await _unitOfWork.CompleteAsync();
return Json(new { success = true, message = "Worker assigned successfully", workerName });
}