Starting point for the ARCHITECTURE_AND_CODE_QUALITY.md cleanup pass: a verified (not grep-only) list of layering violations, validation coverage gaps against the existing FluentValidation pattern, and N+1/query-shape issues found so far, plus what was checked and ruled out. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
6.2 KiB
Backend architecture refactor — findings
Tracking doc for refactor/backend-architecture-cleanup. Each item is verified
against the actual code (not just a grep hit) before being listed here.
Checked off as fixed, with tests, on this branch.
1. Layering / dependency-injection violations
AssetControllerinjectsUserManager<ApplicationUser>via constructor but never uses it anywhere in the file — dead dependency, remove it.ContactControllerdepends on bothIContactServiceandILocationServiceand implements full Location CRUD (GetAllLocationsAsync,CreateLocationAsync,UpdateLocationAsync,DeleteLocationAsync,GetLocationsPagedAsync) even though a dedicatedLocationControllerwith the sameILocationServicealready exists. Move the Location endpoints out ofContactControllerintoLocationController; a Contact controller should depend only onIContactService.ArchitectureTests(G2) currently passes with 0 failures — dependency direction is otherwise clean (no controller/service injectsDbContext/IConfiguration/a concrete class). Re-run after each fix above to confirm it stays green.
2. Validation coverage gap
Established pattern: I{Action}{Feature}Validation : IValidator<{Dto}> +
{Action}{Feature}Validation : AbstractValidator<{Dto}> in
SeaHaven.Services/Validation/, injected into the owning business service
(see LocationValidation.cs / LocationService). Confirmed: validation
stays in the service layer (not moved to data services) — only extending
coverage to features that lack it.
Have a validator today: Account, Asset, Contact, Dispatch, Employee, FollowUp, Location, PMSchedule, Vendor, WorkOrder, WorkOrderBoardCreate.
Confirmed missing (real external Create/Update DTO, no validator):
CalendarService.CreateEventAsync/UpdateEventAsyncCompletionDocTemplateService.CreateAsync/UpdateAsyncQuotesService.CreateQuoteAsync/UpdateQuoteAsyncServicesRegistryService.CreateAsync/UpdateAsyncTaskListTemplateService.CreateAsync/UpdateAsyncTeamMemberInviteService.CreateAsyncTeamMemberService.CreateAsync/UpdateAsyncVendorCompanyRosterService.CreateRosterAsyncVendorOperationsService.UpdateAvailabilityAsync/UpdateCommercialStatusAsyncVendorPortalService.UpdateChecklistItemAsyncWorkOrderCommentService.UpdateCommentAsyncWorkOrderDispatchService.UpdateDispatchAsync/UpdateChecklistItemAsyncWorkOrderMediaService.UpdateMediaCategoryAsyncWorkOrderPocService.UpdatePocAsyncWorkOrderUpliftService.CreateAsyncDropdownOptionsService.CreateAsync/UpdateAsyncAuthenticationService.UpdateProfileAsync
Ruled out (internal/no external DTO, not a validator candidate):
AuthenticationService.CreateSessionAsync, VendorPortalTokenService.GetOrCreateActiveTokenAsync,
WorkOrderAccountResolver (no real Create/Update method; grep false positive).
3. N+1 / query-shape issues (verified, not grep-only)
WorkOrderService.DeleteWorkOrderCascadeAsyncloads all comments then calls_commentDataService.DeleteAsync(comment.Id)once per comment.CommentDataService.DeleteAsyncdoes its ownGetByIdAsync(1 SELECT) +SaveChangesAsync(1 commit) per call — N comments = 2N round trips and N separate commits, breaking the one-commit convention (§4) as well as being slow. Fix: add a batch delete (DeleteAllForWorkOrderAsyncviaExecuteDeleteAsync, or load once +RemoveRange+ oneSaveChangesAsync) and call it once.WorkOrderService.SaveAttachmentsAndPhotosloops over uploaded attachments calling_workOrderDataService.AddWorkOrderAttachmentAsyncper file; that method doesAddAsync+SaveChangesAsyncper call — N attachments = N separate commits. Fix: collect theWorkOrderAttachmentsentities in the loop (file storage save can stay per-file — that's I/O, not DB), then add a batch method that callsAddRangeAsync+ oneSaveChangesAsync.VendorCompanyRosterDataService(3 call sites: roster update, create, and a third) loadsexistingTechniciansonce via a single query, then doesexistingTechnicians.FirstOrDefault(e => e.Id == ...)inside a loop over the incoming roster — O(n·m) in-memory scan. Fix: buildexistingTechnicians.ToDictionary(e => e.Id)once before the loop andTryGetValueinside it — O(n+m).
Ruled out during the sweep (false positives / legitimate design)
WorkOrderAuditDataService.PersistAsync: theforeachdetaches conflicting entries from a caughtDbUpdateException; the retrySaveChangesAsyncis after the loop, not inside it. Not an issue.VendorPortalTokenService.RotateAsync/RevokeAllAsync: theforeachonly mutates already-tracked entities in memory (t.RevokedAt = now); the singleSaveChangesAsyncis after the loop. Not an issue.WorkOrderWeekRolledService: callsTryProcessWeekRolledAsynconce per work order inside a batch-job loop, each with its own try/catch. This is intentional per-item fault isolation for a background job — collapsing it into one transaction would change the failure semantics (one bad work order would abort the whole batch). Not an issue; do not "optimize" this away.- Legacy
SeaHavenIndustriesproject (old Blazor admin app) has its own ad-hocServiceclasses touchingDbContextdirectly, violating the layering model wholesale — but it is being actively sunset (see commit64ca9e0, "add legacy deprecation middleware and Blazor WO sunset guard"). Out of scope: refactoring code on its way out is wasted effort.
Still to audit
- Cancellation-token forwarding (§6) on data-service methods — not yet swept.
- Error disclosure (§5) — spot-check only so far (
VendorCompanyRosterControllercatchesDbUpdateConcurrencyExceptiondirectly; check whether that duplicates the existingConcurrencyExceptionFilter). - Tenant-scope derivation (§2) — not yet swept.
AsNoTracking()coverage on read-only data-service queries — not yet swept.