Filters, Areas, and Configuration
Three "structure" topics that show up on day one of a real codebase: filters (cross-cutting behavior around actions), areas (folder-level organization for big apps), and configuration (how the same code runs correctly in Dev, Test, and Production).
Filters: cross-cutting code in the right place
Filters run at defined points around action execution, so per-request concerns don't get copy-pasted into every action:
- Authorization filters run first.
[Authorize]/[Authorize(Roles = "Approver")]is the daily driver; in an agency app it's typically backed by Windows/AD groups or claims. The safe pattern is authorize-by-default: register[Authorize]globally and opt public pages out with[AllowAnonymous]β forgetting an attribute then fails closed. - Action filters run before/after the action β audit logging ("user X ran action Y"), request timing, feeding common ViewBag data.
- Exception filters catch what actions throw β though the exception-handling middleware usually covers this app-wide; write an exception filter only for MVC-specific handling.
- Result filters wrap view/JSON rendering β rarely written, occasionally read.
Filters apply at action, controller, or global scope. When you see behavior you can't find in
the action β a redirect, an extra log line β check the controller's attributes and the global
filter registration (AddControllersWithViews(options => options.Filters.Add(...)) in
Program.cs) before doubting reality.
Areas: keeping a big app navigable
An area is a self-contained MVC slice β its own Controllers/Views/Models folders β for a
major functional chunk: Areas/Admin, Areas/Reports, Areas/Permits. URLs gain a prefix
(/admin/users/edit/5), and teams stop colliding in one giant Controllers folder. Two habits
keep areas painless: always specify the area in cross-area links
(<a asp-area="Admin" asp-controller="Users" asp-action="Index"> β omitting asp-area is the
classic wrong-link-from-inside-an-area bug), and don't share views across areas; shared UI
belongs in partials/layouts at the root.
Configuration and environments
The same build must behave differently per environment β connection strings, service URLs,
feature flags. Configuration is layered IConfiguration: appsettings.json holds the
defaults, appsettings.{Environment}.json overrides per environment (Development, Staging,
Production β selected by ASPNETCORE_ENVIRONMENT), and environment variables override both,
which is how deployment pipelines and containers inject the real values. No config transforms, no
rebuild per environment β the same artifact runs everywhere. For access, the options pattern
(IOptions<SmtpSettings> bound from a config section) beats magic strings scattered through
code: typed, injectable, testable. Legacy note: if you inherit a System.Web app, the equivalent
machinery is web.config plus config transforms.
Two rules that matter double in public sector: secrets never live in source control β real connection strings and API keys come from Azure DevOps variable groups/pipeline substitution at deploy time (or Key Vault), and dev secrets from User Secrets or untracked files. And fail loudly on missing config at startup β a null setting that surfaces three layers deep at 2 a.m. is the avoidable version of that incident.