MVC (Model-View-Controller) is a design pattern that separates your application into three interconnected components. It's the foundation of ASP.NET MVC, ASP.NET Core, Ruby on Rails, Laravel, Django, and countless other web frameworks.
If you've ever wondered why web frameworks are structured the way they are, MVC is the answer.
Here's what MVC is, why it exists, and how it works—explained with code examples.
What is MVC?
MVC stands for Model-View-Controller, a design pattern that divides an application into three components:
- Model — Represents data and business logic
- View — Represents the user interface (what users see)
- Controller — Handles user input and coordinates Model and View
The goal: Separate concerns so each component has one responsibility.
// Model: Represents data
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
// Controller: Handles requests
public class ProductsController : Controller
{
public IActionResult Index()
{
var products = _database.GetAllProducts();
return View(products);
}
}
// View: Displays data (Razor syntax)
@model List<Product>
<h1>Products</h1>
@foreach (var product in Model)
{
<p>@product.Name - @product.Price.ToString("C")</p>
}
Result: User visits /products, Controller fetches data (Model), passes it to View, View renders HTML.
Why Does MVC Exist?
The Problem Before MVC
In the early days of web development (1990s-2000s), code looked like this:
<!-- products.php (everything mixed together) -->
<html>
<body>
<h1>Products</h1>
<?php
// Database query in the HTML file
$conn = mysqli_connect("localhost", "user", "password", "shop");
$result = mysqli_query($conn, "SELECT * FROM products");
while ($row = mysqli_fetch_assoc($result)) {
// Business logic mixed with presentation
$price = $row['price'] * 1.10; // Add 10% tax
echo "<p>" . $row['name'] . " - $" . $price . "</p>";
}
mysqli_close($conn);
?>
</body>
</html>
Problems with this approach:
- Impossible to test — Business logic mixed with HTML, can't unit test
- Hard to maintain — Change tax calculation? Edit every page
- No reusability — Can't reuse tax logic in API or mobile app
- Designer/developer conflict — Designers can't edit HTML without breaking PHP logic
MVC solved these problems.
The Solution: Separation of Concerns
MVC separates code by responsibility:
| Component | Responsibility | Example |
|---|---|---|
| Model | Data and business logic | Calculate tax, validate email, fetch from database |
| View | Presentation logic | HTML templates, formatting, styling |
| Controller | Request handling | Route /products to correct Model and View |
Benefits:
✅ Testable — Test business logic without rendering HTML
✅ Maintainable — Change tax logic in one place (Model)
✅ Reusable — Use same Model for web, API, mobile
✅ Parallel development — Designers work on Views, developers on Models
How MVC Works: The Flow
User Request Flow
Example: User visits https://example.com/products/5
-
Request hits Controller
- Router maps
/products/5toProductsController.Details(5)
- Router maps
-
Controller fetches data from Model
- Controller calls
_database.GetProduct(5) - Model retrieves data from database
- Controller calls
-
Controller passes data to View
- Controller returns
View(product)
- Controller returns
-
View renders HTML
- View receives
productdata - View generates HTML with product details
- View receives
-
HTML returned to user
- User sees product page
// 1. Controller receives request
public class ProductsController : Controller
{
private readonly IProductRepository _repository;
public ProductsController(IProductRepository repository)
{
_repository = repository;
}
public IActionResult Details(int id)
{
// 2. Controller fetches data from Model
var product = _repository.GetById(id);
if (product == null)
return NotFound();
// 3. Controller passes data to View
return View(product);
}
}
// 2. Model represents data and business logic
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
// Business logic method
public decimal GetPriceWithTax(decimal taxRate)
{
return Price * (1 + taxRate);
}
}
// 4. View renders HTML (Razor syntax)
@model Product
<h1>@Model.Name</h1>
<p>Price: @Model.Price.ToString("C")</p>
<p>Price with Tax: @Model.GetPriceWithTax(0.10m).ToString("C")</p>
Result: User sees product details page with tax calculated correctly.
Breaking Down Each Component
The Model
Responsibility: Represent data and business logic.
What Models do:
- Define data structure (properties)
- Validate data (e.g., email format, required fields)
- Implement business rules (e.g., calculate discounts, apply tax)
- Interact with database (via repository or ORM)
Example Model:
public class Order
{
public int Id { get; set; }
public DateTime Date { get; set; }
public List<OrderItem> Items { get; set; } = new();
public string CustomerEmail { get; set; }
// Business logic: Calculate total
public decimal GetTotal()
{
return Items.Sum(item => item.Price * item.Quantity);
}
// Business logic: Calculate tax
public decimal GetTax(decimal taxRate)
{
return GetTotal() * taxRate;
}
// Business logic: Validate order
public bool IsValid()
{
if (Items.Count == 0) return false;
if (string.IsNullOrEmpty(CustomerEmail)) return false;
if (!IsValidEmail(CustomerEmail)) return false;
return true;
}
private bool IsValidEmail(string email)
{
return email.Contains("@") && email.Contains(".");
}
}
public class OrderItem
{
public int ProductId { get; set; }
public string ProductName { get; set; }
public decimal Price { get; set; }
public int Quantity { get; set; }
}
Key point: Models contain logic, not just data. They're not "dumb" data containers.
The View
Responsibility: Display data to the user.
What Views do:
- Render HTML based on Model data
- Format data for display (e.g., currency, dates)
- Display validation errors
- Provide forms for user input
Example View (Razor):
@model Order
<!DOCTYPE html>
<html>
<head>
<title>Order Confirmation</title>
<style>
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px; }
th { background-color: #f2f2f2; }
</style>
</head>
<body>
<h1>Order Confirmation</h1>
<p>Order Date: @Model.Date.ToString("MMMM dd, yyyy")</p>
<p>Customer Email: @Model.CustomerEmail</p>
<h2>Items</h2>
<table>
<tr>
<th>Product</th>
<th>Price</th>
<th>Quantity</th>
<th>Total</th>
</tr>
@foreach (var item in Model.Items)
{
<tr>
<td>@item.ProductName</td>
<td>@item.Price.ToString("C")</td>
<td>@item.Quantity</td>
<td>@((item.Price * item.Quantity).ToString("C"))</td>
</tr>
}
</table>
<h3>Total: @Model.GetTotal().ToString("C")</h3>
<h3>Tax: @Model.GetTax(0.10m).ToString("C")</h3>
<h3>Grand Total: @((Model.GetTotal() + Model.GetTax(0.10m)).ToString("C"))</h3>
</body>
</html>
Key point: Views display data but don't contain business logic. Logic stays in Models.
The Controller
Responsibility: Handle user requests and coordinate Model and View.
What Controllers do:
- Receive HTTP requests (GET, POST, PUT, DELETE)
- Validate user input
- Call Models to fetch/update data
- Select which View to render
- Pass data from Model to View
Example Controller:
using Microsoft.AspNetCore.Mvc;
public class OrdersController : Controller
{
private readonly IOrderRepository _orderRepository;
public OrdersController(IOrderRepository orderRepository)
{
_orderRepository = orderRepository;
}
// GET: /orders/5
public IActionResult Details(int id)
{
var order = _orderRepository.GetById(id);
if (order == null)
return NotFound();
return View(order);
}
// GET: /orders/create
public IActionResult Create()
{
return View();
}
// POST: /orders/create
[HttpPost]
public IActionResult Create(Order order)
{
// Validate using Model's business logic
if (!order.IsValid())
{
ModelState.AddModelError("", "Order is invalid");
return View(order);
}
// Save to database via Model/Repository
_orderRepository.Add(order);
// Redirect to confirmation page
return RedirectToAction("Confirmation", new { id = order.Id });
}
// GET: /orders/confirmation/5
public IActionResult Confirmation(int id)
{
var order = _orderRepository.GetById(id);
if (order == null)
return NotFound();
return View(order);
}
}
Key point: Controllers are thin. They delegate business logic to Models, not implement it themselves.
MVC in ASP.NET Core
ASP.NET Core implements MVC natively:
// Program.cs (ASP.NET Core 8.0)
var builder = WebApplication.CreateBuilder(args);
// Add MVC services
builder.Services.AddControllersWithViews();
var app = builder.Build();
// Configure routing
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
Folder structure:
MyApp/
├── Controllers/
│ ├── HomeController.cs
│ ├── ProductsController.cs
│ └── OrdersController.cs
├── Models/
│ ├── Product.cs
│ ├── Order.cs
│ └── OrderItem.cs
├── Views/
│ ├── Home/
│ │ └── Index.cshtml
│ ├── Products/
│ │ ├── Index.cshtml
│ │ └── Details.cshtml
│ └── Orders/
│ ├── Create.cshtml
│ └── Confirmation.cshtml
└── Program.cs
MVC vs Other Patterns
MVC vs MVVM (Model-View-ViewModel)
MVVM is used in client-side frameworks (WPF, Xamarin, Blazor).
| Pattern | Use Case | Data Binding |
|---|---|---|
| MVC | Server-side web apps | One-way (Model → View) |
| MVVM | Client-side apps | Two-way (View ↔ ViewModel) |
Example (Blazor MVVM):
// ViewModel
public class CounterViewModel
{
public int Count { get; set; } = 0;
public void Increment()
{
Count++;
}
}
// View (Razor component)
@page "/counter"
<h1>Counter: @ViewModel.Count</h1>
<button @onclick="ViewModel.Increment">Increment</button>
@code {
private CounterViewModel ViewModel = new();
}
Key difference: MVVM has two-way binding (user clicks button, ViewModel updates, View updates automatically).
MVC vs MVP (Model-View-Presenter)
MVP is similar to MVC but the Presenter has more control.
| Pattern | Controller/Presenter Role |
|---|---|
| MVC | Controller routes requests to Model and View |
| MVP | Presenter directly updates View (no templating) |
MVP is less common in web development.
Common MVC Mistakes
Mistake #1: Fat Controllers
Bad (business logic in Controller):
public class OrdersController : Controller
{
[HttpPost]
public IActionResult Create(Order order)
{
// Business logic in Controller (BAD!)
if (order.Items.Count == 0)
{
ModelState.AddModelError("", "Order must have at least one item");
return View(order);
}
decimal total = 0;
foreach (var item in order.Items)
{
total += item.Price * item.Quantity;
}
decimal tax = total * 0.10m;
// ... more logic ...
return View();
}
}
Good (business logic in Model):
// Model
public class Order
{
public bool IsValid() => Items.Count > 0;
public decimal GetTotal() => Items.Sum(i => i.Price * i.Quantity);
public decimal GetTax(decimal rate) => GetTotal() * rate;
}
// Controller
public class OrdersController : Controller
{
[HttpPost]
public IActionResult Create(Order order)
{
if (!order.IsValid())
{
ModelState.AddModelError("", "Order is invalid");
return View(order);
}
_orderRepository.Add(order);
return RedirectToAction("Confirmation");
}
}
Mistake #2: Business Logic in Views
Bad:
@model Order
<h3>Total: @(Model.Items.Sum(i => i.Price * i.Quantity).ToString("C"))</h3>
<h3>Tax: @((Model.Items.Sum(i => i.Price * i.Quantity) * 0.10m).ToString("C"))</h3>
Good:
@model Order
<h3>Total: @Model.GetTotal().ToString("C")</h3>
<h3>Tax: @Model.GetTax(0.10m).ToString("C")</h3>
Mistake #3: Direct Database Access in Controllers
Bad:
public class ProductsController : Controller
{
public IActionResult Index()
{
// Direct SQL in Controller (BAD!)
var connection = new SqlConnection("...");
var command = new SqlCommand("SELECT * FROM Products", connection);
// ... execute query ...
return View(products);
}
}
Good:
public class ProductsController : Controller
{
private readonly IProductRepository _repository;
public ProductsController(IProductRepository repository)
{
_repository = repository;
}
public IActionResult Index()
{
var products = _repository.GetAll();
return View(products);
}
}
The Bottom Line: Why MVC Matters
MVC separates concerns:
- Models — Business logic and data
- Views — Presentation
- Controllers — Request handling
Benefits:
✅ Testable — Unit test Models without Views
✅ Maintainable — Change logic in one place
✅ Reusable — Use Models in web, API, mobile
✅ Parallel development — Teams work independently
MVC is the foundation of modern web frameworks:
- ASP.NET Core MVC
- Ruby on Rails
- Laravel (PHP)
- Django (Python)
- Spring MVC (Java)
If you're building web applications in .NET, understanding MVC is essential.
Written by Jacob Mellor, CTO at Iron Software. Jacob created IronPDF and leads a team of 50+ engineers building .NET document processing libraries.
Top comments (0)