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:
@@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using PowderCoating.Core.Entities;
|
||||
using PowderCoating.Core.Interfaces;
|
||||
using PowderCoating.Core.Interfaces.Services;
|
||||
using PowderCoating.Infrastructure.Data;
|
||||
using PowderCoating.Infrastructure.Repositories;
|
||||
using PowderCoating.Infrastructure.Services;
|
||||
@@ -209,6 +210,11 @@ builder.Services.AddScoped<ICompanyLogoService, CompanyLogoService>();
|
||||
builder.Services.AddScoped<IEquipmentManualService, EquipmentManualService>();
|
||||
builder.Services.AddScoped<IPricingCalculationService, PricingCalculationService>();
|
||||
builder.Services.AddScoped<IPowderInsightsService, PowderInsightsService>();
|
||||
builder.Services.AddScoped<IAuditLogService, AuditLogService>();
|
||||
builder.Services.AddScoped<IAiUsageReportService, AiUsageReportService>();
|
||||
builder.Services.AddScoped<IDashboardReadService, DashboardReadService>();
|
||||
builder.Services.AddScoped<ICompanyListService, CompanyListService>();
|
||||
builder.Services.AddScoped<ICompanyDataPurgeService, CompanyDataPurgeService>();
|
||||
builder.Services.AddScoped<IPdfService, PdfService>();
|
||||
builder.Services.AddScoped<ISeedDataService, SeedDataService>();
|
||||
builder.Services.AddScoped<ICompanyConfigHealthService, CompanyConfigHealthService>();
|
||||
@@ -835,6 +841,11 @@ using (var scope = app.Services.CreateScope())
|
||||
// Fail fast with a clear message rather than a cryptic runtime error later.
|
||||
ValidateRequiredConfiguration(app.Configuration, app.Environment);
|
||||
|
||||
// ── Data access architecture enforcement ─────────────────────────────────────
|
||||
// Throws at startup if any non-exempt controller injects ApplicationDbContext directly.
|
||||
// This is the Phase 4 gate: the app cannot start with a violation.
|
||||
EnforceDataAccessArchitecture();
|
||||
|
||||
try
|
||||
{
|
||||
Log.Information("Starting web application");
|
||||
@@ -894,6 +905,61 @@ static void ValidateRequiredConfiguration(IConfiguration config, IWebHostEnviron
|
||||
}
|
||||
}
|
||||
|
||||
// ── Data access architecture enforcement ─────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// Scans every Controller subclass in the Web assembly at startup and throws if any
|
||||
/// non-exempt controller declares ApplicationDbContext as a constructor parameter.
|
||||
/// This enforces the rule defined in docs/DATA_ACCESS_ARCHITECTURE.md — if a developer
|
||||
/// adds a new controller that injects ApplicationDbContext directly, the app will refuse
|
||||
/// to start with a clear message naming the violator.
|
||||
/// </summary>
|
||||
static void EnforceDataAccessArchitecture()
|
||||
{
|
||||
// Controllers in this set are documented permanent exceptions — see DATA_ACCESS_ARCHITECTURE.md.
|
||||
var permanentExceptions = new HashSet<string>
|
||||
{
|
||||
"StripeWebhookController",
|
||||
"WebhooksController",
|
||||
"PaymentController",
|
||||
"RegistrationController",
|
||||
"DataExportController",
|
||||
"AccountDataExportController",
|
||||
"DataPurgeController",
|
||||
"SystemInfoController",
|
||||
"SystemLogsController",
|
||||
"CompanyHealthController",
|
||||
"PasskeyController",
|
||||
"AuditLogController",
|
||||
"UserActivityController",
|
||||
"EmailBroadcastController",
|
||||
"RevenueController",
|
||||
"StripeEventsController",
|
||||
"SubscriptionManagementController",
|
||||
"UsageQuotaController",
|
||||
};
|
||||
|
||||
var violators = typeof(Program).Assembly.GetTypes()
|
||||
.Where(t => t.IsClass && !t.IsAbstract
|
||||
&& typeof(Microsoft.AspNetCore.Mvc.Controller).IsAssignableFrom(t)
|
||||
&& !permanentExceptions.Contains(t.Name))
|
||||
.Where(t => t.GetConstructors()
|
||||
.Any(ctor => ctor.GetParameters()
|
||||
.Any(p => p.ParameterType == typeof(ApplicationDbContext))))
|
||||
.Select(t => t.Name)
|
||||
.OrderBy(n => n)
|
||||
.ToList();
|
||||
|
||||
if (violators.Count == 0) return;
|
||||
|
||||
var names = string.Join(", ", violators);
|
||||
throw new InvalidOperationException(
|
||||
$"DATA ACCESS VIOLATION — {violators.Count} controller(s) inject ApplicationDbContext directly " +
|
||||
$"and are not in the permanent exceptions list.\n" +
|
||||
$"Violators: {names}\n" +
|
||||
$"Fix: route data access through IUnitOfWork. " +
|
||||
$"To add a permanent exception, update both the controller comment and docs/DATA_ACCESS_ARCHITECTURE.md.");
|
||||
}
|
||||
|
||||
// ── Serilog DB sink column configuration ─────────────────────────────────────────
|
||||
static ColumnOptions BuildLogColumnOptions()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user