DEV Community

IronSoftware
IronSoftware

Posted on

What is MVC? The Model-View-Controller Pattern Explained

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:

  1. Model — Represents data and business logic
  2. View — Represents the user interface (what users see)
  3. 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>
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Problems with this approach:

  1. Impossible to test — Business logic mixed with HTML, can't unit test
  2. Hard to maintain — Change tax calculation? Edit every page
  3. No reusability — Can't reuse tax logic in API or mobile app
  4. 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

  1. Request hits Controller

    • Router maps /products/5 to ProductsController.Details(5)
  2. Controller fetches data from Model

    • Controller calls _database.GetProduct(5)
    • Model retrieves data from database
  3. Controller passes data to View

    • Controller returns View(product)
  4. View renders HTML

    • View receives product data
    • View generates HTML with product details
  5. 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>
Enter fullscreen mode Exit fullscreen mode

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; }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Good:

@model Order

<h3>Total: @Model.GetTotal().ToString("C")</h3>
<h3>Tax: @Model.GetTax(0.10m).ToString("C")</h3>
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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)