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
@@ -10,7 +10,6 @@ using PowderCoating.Application.DTOs.Vendor;
using PowderCoating.Core.Entities;
using PowderCoating.Core.Enums;
using PowderCoating.Core.Interfaces;
using PowderCoating.Infrastructure.Data;
namespace PowderCoating.Web.Controllers;
@@ -28,20 +27,17 @@ public class VendorsController : Controller
private readonly IMapper _mapper;
private readonly UserManager<ApplicationUser> _userManager;
private readonly ILogger<VendorsController> _logger;
private readonly ApplicationDbContext _context;
public VendorsController(
IUnitOfWork unitOfWork,
IMapper mapper,
UserManager<ApplicationUser> userManager,
ILogger<VendorsController> logger,
ApplicationDbContext context)
ILogger<VendorsController> logger)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
_userManager = userManager;
_logger = logger;
_context = context;
}
/// <summary>
@@ -163,7 +159,7 @@ public class VendorsController : Controller
var vendorDto = _mapper.Map<VendorDto>(vendor);
if (vendor.DefaultExpenseAccountId.HasValue)
{
var acct = await _context.Accounts.FindAsync(vendor.DefaultExpenseAccountId.Value);
var acct = await _unitOfWork.Accounts.GetByIdAsync(vendor.DefaultExpenseAccountId.Value);
vendorDto.DefaultExpenseAccountName = acct != null ? $"{acct.AccountNumber} {acct.Name}" : null;
}
return View(vendorDto);
@@ -395,14 +391,13 @@ public class VendorsController : Controller
/// </summary>
private async Task PopulateExpenseAccountsAsync()
{
var accounts = await _context.Accounts
.Where(a => !a.IsDeleted && a.IsActive &&
(a.AccountType == AccountType.Expense ||
a.AccountType == AccountType.CostOfGoods ||
a.AccountType == AccountType.Asset))
var accounts = (await _unitOfWork.Accounts.FindAsync(
a => a.IsActive && (a.AccountType == AccountType.Expense ||
a.AccountType == AccountType.CostOfGoods ||
a.AccountType == AccountType.Asset)))
.OrderBy(a => a.AccountNumber)
.Select(a => new SelectListItem($"{a.AccountNumber} {a.Name}", a.Id.ToString()))
.ToListAsync();
.ToList();
accounts.Insert(0, new SelectListItem("— None —", ""));
ViewBag.ExpenseAccounts = accounts;