Layers That Earn Their Keep
Nearly every public-sector MVC application you'll inherit follows the same shape: controller β service β repository/EF β database, with view models crossing the top boundary and entities living below it. The architecture isn't fashionable, but it survives because it answers one question consistently: where does this code go? Your job is to keep that answer crisp.
What each layer owns
- Controllers translate HTTP into intent. They bind and validate input, call one service
method, and turn the result into a view or JSON. A healthy action is 5β15 lines. No LINQ-to-
entities, no business rules, no
DbContextβ a controller that queries the database directly has skipped two layers, and every rule it embeds becomes untestable and un-reusable. - Services own the business logic: rules, calculations, orchestration ("approving a permit checks eligibility, writes the status change, and queues the notification"). Services accept and return simple types or DTOs, throw meaningful exceptions or return result objects, and know nothing about HTTP.
- Repository / EF owns persistence. Whether your project wraps EF in repository classes or
treats
DbContextitself as the data layer (both are common and both are fine), the point is the same: query composition and mapping to entities happen here, not upstairs. - The database owns durability, constraints, and β in most agencies β a DBA-reviewed schema that outlives the application code.
View models at the boundary
Views and grids consume view models, never entities. This single discipline prevents a
remarkable amount of pain: over-posting attacks (a crafted POST setting IsApproved=true on a
bound entity), lazy-loading queries firing from Razor at render time, and serializer cycles in
JSON endpoints. The flow is mechanical: entity β view model on the way out (often via
AutoMapper's ProjectTo, lesson 3), view model β validated changes on a fetched entity on the
way in.
Keeping logic out of controllers and views
The test for a controller: could you call this same operation from a nightly batch job or a
Web API endpoint without copying code? If not, the logic is in the wrong place. The test for a
view: does it only format what the view model already decided? An @if chain in Razor that
computes whether a case is overdue is a business rule hiding in the presentation layer β move
it to the service, expose a bool IsOverdue on the view model, and let the view stay dumb.
Layers cost indirection, and the payoff is testability and predictability: services get unit tests without a web server, rules exist in exactly one place, and the next developer β possibly you, in a year β finds code where the architecture says it should be. A layer that doesn't buy that (a pass-through service that only forwards calls, a repository that only renames DbSet methods) is ceremony; it's fine to keep such layers thin as long as they stay consistent across the codebase.