Cross-Cutting Concerns
Validation, logging, caching, transactions β every feature needs them, no feature owns them. In a large codebase the danger isn't doing these things badly; it's doing them five different ways. This lesson is about the standard mechanisms and about picking one per concern.
The core four
- Validation. Two rings: input shape (DataAnnotations or FluentValidation on view models,
checked via
ModelState.IsValidat the controller) and business rules ("permit can't be approved with unpaid fees") which belong in services β they must hold no matter which entry point calls. Client-side validation is convenience; server-side is the contract. - Logging. One abstraction everywhere β
ILogger<T>, with Serilog or NLog plugged in as a provider. Log structured context (case ID, user, operation), let exception-handling middleware log unhandled exceptions once, and never let a logging failure break the request. - Caching. Two flavors: output caching for whole responses (the output-caching middleware
plus
[OutputCache]β good for anonymous, rarely-changing pages) and data caching (IMemoryCache) for expensive lookups like code tables. Every cache entry needs an answer to "how does this invalidate?" before it ships; stale lookup data in a government system is a correctness bug, not a performance detail. - Transactions / unit of work.
DbContextis a unit of work: one instance per request, changes accumulated, oneSaveChangesper business operation (Module 5). Most codebases need no extra UnitOfWork abstraction on top β reserve explicit transactions for genuinely multi-step saves.
Where the code hooks in
Three mechanisms, in order of preference:
- Filters β MVC's native interception for anything HTTP-shaped: authorization, model-state short-circuiting, action timing, exception translation. They apply globally or per-controller, and are the workhorse for web cross-cutting.
- Decorators β a wrapper class implementing the same interface (a
CachedLookupServicearoundLookupService) registered in the container. Best when the concern belongs to the service layer and must apply even for non-HTTP callers like batch jobs. - Middleware β the ASP.NET Core pipeline for request-level concerns before MVC is even
involved: correlation IDs, global exception handling, security headers. (Legacy note: this is
what
HttpModules andGlobal.asaxevents did in the System.Web era.)
Rule of thumb: request-level β middleware or global filters; MVC-level β filters; domain-level β decorators or explicit service code. Base-controller inheritance and copy-pasted try/catch blocks are the mechanisms to retire.
Consistency is the feature
On a ten-year codebase, the second way of doing anything is a bug factory: half the actions logged, two cache key formats, validation that runs on some paths. When you join a team, find the established pattern for each concern and follow it β even if you'd have chosen differently. Propose changes as migrations with an owner, not as a sixth pattern. Boring and uniform beats clever and varied everywhere that operations, audits, and 2 a.m. debugging are real.