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:
@@ -0,0 +1,103 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using PowderCoating.Core.Entities;
|
||||
using PowderCoating.Core.Interfaces.Services;
|
||||
using PowderCoating.Infrastructure.Data;
|
||||
|
||||
namespace PowderCoating.Infrastructure.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Implements <see cref="ICompanyListService"/> using <see cref="ApplicationDbContext"/> directly.
|
||||
/// Queries require <c>IgnoreQueryFilters()</c> (to bypass the tenant filter and see all companies),
|
||||
/// dynamic sort expressions, and cross-entity GROUP BY aggregations — all of which are beyond the
|
||||
/// generic <see cref="PowderCoating.Core.Interfaces.IRepository{T}"/>.
|
||||
/// </summary>
|
||||
public class CompanyListService : ICompanyListService
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public CompanyListService(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<(List<Company> Companies, int TotalCount)> GetPagedAsync(
|
||||
string? searchTerm, string sortColumn, string sortDirection, int page, int pageSize)
|
||||
{
|
||||
var query = _context.Companies
|
||||
.AsNoTracking()
|
||||
.IgnoreQueryFilters()
|
||||
.Where(c => !c.IsDeleted)
|
||||
.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(searchTerm))
|
||||
{
|
||||
var s = searchTerm.ToLower();
|
||||
query = query.Where(c =>
|
||||
c.CompanyName.ToLower().Contains(s) ||
|
||||
(c.CompanyCode != null && c.CompanyCode.ToLower().Contains(s)) ||
|
||||
(c.PrimaryContactEmail != null && c.PrimaryContactEmail.ToLower().Contains(s)) ||
|
||||
(c.Phone != null && c.Phone.ToLower().Contains(s)));
|
||||
}
|
||||
|
||||
query = (sortColumn, sortDirection == "asc") switch
|
||||
{
|
||||
("CompanyName", true) => query.OrderBy(c => c.CompanyName),
|
||||
("CompanyName", false) => query.OrderByDescending(c => c.CompanyName),
|
||||
("Plan", true) => query.OrderBy(c => c.SubscriptionPlan),
|
||||
("Plan", false) => query.OrderByDescending(c => c.SubscriptionPlan),
|
||||
("Status", true) => query.OrderBy(c => c.IsActive),
|
||||
("Status", false) => query.OrderByDescending(c => c.IsActive),
|
||||
("Created", true) => query.OrderBy(c => c.CreatedAt),
|
||||
("Created", false) => query.OrderByDescending(c => c.CreatedAt),
|
||||
_ => query.OrderBy(c => c.CompanyName)
|
||||
};
|
||||
|
||||
var totalCount = await query.CountAsync();
|
||||
|
||||
var companies = await query
|
||||
.Include(c => c.Users)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
return (companies, totalCount);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<CompanyCountSummary> GetCountSummaryAsync(IReadOnlyList<int> companyIds)
|
||||
{
|
||||
var jobCounts = await _context.Jobs
|
||||
.IgnoreQueryFilters()
|
||||
.Where(j => companyIds.Contains(j.CompanyId) && !j.IsDeleted)
|
||||
.GroupBy(j => j.CompanyId)
|
||||
.Select(g => new { g.Key, Count = g.Count() })
|
||||
.ToDictionaryAsync(x => x.Key, x => x.Count);
|
||||
|
||||
var quoteCounts = await _context.Quotes
|
||||
.IgnoreQueryFilters()
|
||||
.Where(q => companyIds.Contains(q.CompanyId) && !q.IsDeleted)
|
||||
.GroupBy(q => q.CompanyId)
|
||||
.Select(g => new { g.Key, Count = g.Count() })
|
||||
.ToDictionaryAsync(x => x.Key, x => x.Count);
|
||||
|
||||
var customerCounts = await _context.Customers
|
||||
.IgnoreQueryFilters()
|
||||
.Where(c => companyIds.Contains(c.CompanyId) && !c.IsDeleted)
|
||||
.GroupBy(c => c.CompanyId)
|
||||
.Select(g => new { g.Key, Count = g.Count() })
|
||||
.ToDictionaryAsync(x => x.Key, x => x.Count);
|
||||
|
||||
var wizardRaw = await _context.CompanyPreferences
|
||||
.IgnoreQueryFilters()
|
||||
.Where(p => companyIds.Contains(p.CompanyId) && p.SetupWizardCompleted)
|
||||
.Select(p => new { p.CompanyId, p.SetupWizardCompletedAt, p.SetupWizardCompletedByName })
|
||||
.ToListAsync();
|
||||
|
||||
var wizardInfo = wizardRaw.ToDictionary(
|
||||
x => x.CompanyId,
|
||||
x => new CompanyWizardInfo(true, x.SetupWizardCompletedAt, x.SetupWizardCompletedByName));
|
||||
|
||||
return new CompanyCountSummary(jobCounts, quoteCounts, customerCounts, wizardInfo);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user