The Model-View-Controller (MVC) pattern divides an application into three interconnected roles. The Model represents the data and business rules. The View renders the visual presentation (HTML for web applications). The Controller handles user input (HTTP requests), retrieves or updates the model, and selects the view to render. This separation of concerns makes each part independently testable, independently modifiable, and clear in its responsibility. ASP.NET Core MVC is Microsoft’s production-grade implementation of this pattern for server-rendered web applications.
The Three Roles
// ── MODEL — data and business logic ──────────────────────────────────────
// Models are plain C# classes. In MVC they appear in two forms:
// 1. Domain/entity models (what the database stores)
// 2. View models (shaped specifically for what a view needs to render)
// View model — shaped for a specific view
public class PostIndexViewModel
{
public IReadOnlyList<PostSummary> Posts { get; init; } = [];
public int Page { get; init; }
public int PageSize { get; init; }
public int Total { get; init; }
public bool HasNext => (Page * PageSize) < Total;
public bool HasPrev => Page > 1;
}
public record PostSummary(int Id, string Title, string Slug, DateTime PublishedAt);
// ── CONTROLLER — handles requests, coordinates model and view ─────────────
public class PostsController : Controller // note: Controller, not ControllerBase
{
private readonly IPostService _service;
public PostsController(IPostService service) => _service = service;
// GET /posts (or /posts/index by conventional routing)
public async Task<IActionResult> Index(int page = 1)
{
// 1. Retrieve model data via service
var posts = await _service.GetPublishedAsync(page, pageSize: 10);
var total = await _service.CountPublishedAsync();
// 2. Build view model
var vm = new PostIndexViewModel
{
Posts = posts.Select(p => new PostSummary(p.Id, p.Title, p.Slug, p.PublishedAt.Value)).ToList(),
Page = page,
PageSize = 10,
Total = total,
};
// 3. Return ViewResult — Razor renders Views/Posts/Index.cshtml
return View(vm);
}
}
// ── VIEW — renders HTML (Views/Posts/Index.cshtml) ────────────────────────
// @model PostIndexViewModel ← declares what type this view expects
//
// <h1>Published Posts</h1>
// @foreach (var post in Model.Posts)
// {
// <article>
// <h2><a href="/posts/@post.Slug">@post.Title</a></h2>
// <time>@post.PublishedAt.ToString("MMMM d, yyyy")</time>
// </article>
// }
Controller (not ControllerBase as in Web API). Controller adds view-related methods: View(), PartialView(), RedirectToAction(), TempData, and ViewBag. ControllerBase is the minimal base for API controllers that return JSON. Never inherit from Controller in a Web API controller — it adds unnecessary overhead. Never inherit from ControllerBase in an MVC controller — you lose access to view-related helpers.MVC vs Web API — When to Choose Each
| Concern | ASP.NET Core MVC | ASP.NET Core Web API |
|---|---|---|
| Output format | HTML (Razor views) | JSON (or XML) |
| Client | Browser (server-rendered) | Angular, mobile, other services |
| State | Session, cookies, ViewBag | Stateless, JWT tokens |
| Best for | Admin panels, CMS, marketing sites | REST APIs, SPAs, microservices |
| Base class | Controller | ControllerBase |
| Registration | AddControllersWithViews | AddControllers |
Common Mistakes
Mistake 1 — Inheriting from ControllerBase in an MVC controller
❌ Wrong — View(), TempData, and Redirect helpers are unavailable:
public class HomeController : ControllerBase // wrong base for MVC!
✅ Correct — inherit from Controller for MVC controllers.
Mistake 2 — Passing entity models directly to views (over-posting vulnerability)
❌ Wrong — view accesses all entity properties including sensitive ones; form posts can set any property.
✅ Correct — always create a dedicated ViewModel for each view.