MVC (Model View Controller)
MVC (Model-View-Controller) is a software architectural pattern that separates an application into three interconnected components. Originally formulated in the 1970s at Xerox PARC for building user interfaces in Smalltalk, MVC has evolved and become one of the most widely adopted patterns in web development, including ASP.NET Core applications.
The Three Components
Model
The Model represents the data and business logic of the application. It is responsible for:
- Data structures: Defining entities and their properties
- Business rules: Implementing validation and domain logic
- Data access: Interacting with databases or other data sources
- State management: Maintaining application state
The Model is independent of the user interface—it doesn't know or care how data is displayed.
public class Platform
{
public int Id { get; set; }
public required string PlatformName { get; set; }
public ICollection<Command> Commands { get; set; } = new List<Command>();
}
View
The View is responsible for presenting data to the user. It handles:
- Presentation logic: How data is formatted and displayed
- User interface: The visual representation
- Data binding: Connecting model data to UI elements
Views should contain minimal logic—they receive data from the Controller and render it. They should not directly access or modify the Model.
In traditional web applications, Views are HTML templates (Razor pages in ASP.NET). In Web APIs, the "View" is effectively the JSON or XML serialization of response objects.
{
"id": 5,
"platformName": "Docker"
}
Controller
The Controller acts as an intermediary between Model and View. It:
- Handles requests: Processes incoming HTTP requests
- Orchestrates logic: Coordinates between Model and View
- Updates models: Processes user input and updates the Model
- Selects views: Determines which View to render
- Returns responses: Sends data back to the client
Controllers contain the application flow logic but should remain thin, delegating complex operations to services or the Model layer.
Request Flow
The typical flow of an MVC application follows this pattern:
1. User Request
↓
2. Routing (matches request to Controller action)
↓
3. Controller (processes request)
↓
4. Model (retrieves/updates data)
↓
5. Controller (prepares response)
↓
6. View (renders response)
↓
7. User Response
Example flow for getting a platform:
GET /api/platforms/5
↓
Routing → PlatformsController.GetPlatform(5)
↓
Controller → _repository.GetById(5)
↓
Model → Query database, return Platform object
↓
Controller → return Ok(platform)
↓
View (JSON serializer) → {"id": 5, "name": "Docker", ...}
↓
HTTP 200 OK with JSON body
Benefits of MVC
Separation of Concerns
Each component has a distinct responsibility, making code:
- Easier to understand: Each layer has a clear purpose
- Easier to test: Components can be tested in isolation
- Easier to maintain: Changes to one layer don't cascade to others
Parallel Development
Teams can work on different components simultaneously:
- UI developers work on Views
- Backend developers work on Models and Controllers
- Database developers work on data access
Testability
The separation makes unit testing straightforward:
public class PlatformsControllerTests
{
[Fact]
public void GetPlatform_ReturnsNotFound_WhenPlatformDoesNotExist()
{
// Arrange
var mockRepo = new Mock<IPlatformRepository>();
mockRepo.Setup(r => r.GetById(999)).Returns((Platform)null);
var controller = new PlatformsController(mockRepo.Object);
// Act
var result = controller.GetPlatform(999);
// Assert
Assert.IsType<NotFoundResult>(result.Result);
}
}
Reusability
Models can be reused across different Views and Controllers, and Controllers can serve multiple client types (web, mobile, desktop) by returning appropriate representations.
Best Practices
Keep Controllers Thin
Controllers should orchestrate, not implement business logic:
// ❌ Fat controller
[HttpPost]
public ActionResult<Platform> CreatePlatform(PlatformCreateDto dto)
{
// Validation logic
if (string.IsNullOrEmpty(dto.Name))
return BadRequest("Name is required");
// Business logic
var platform = new Platform
{
Name = dto.Name.Trim(),
Publisher = dto.Publisher,
LicenseType = dto.LicenseType ?? "Unknown"
};
// Data access logic
using var connection = new SqlConnection(_connectionString);
connection.Open();
var command = new SqlCommand(
"INSERT INTO Platforms (Name, Publisher, LicenseType) VALUES (@Name, @Publisher, @LicenseType)",
connection);
// ... parameter setup and execution
return CreatedAtAction(nameof(GetPlatform), new { id = platform.Id }, platform);
}
// ✅ Thin controller
[HttpPost]
public async Task<ActionResult<PlatformDto>> CreatePlatform(PlatformCreateDto dto)
{
var platform = await _repository.CreatePlatformAsync(dto);
return CreatedAtAction(nameof(GetPlatform), new { id = platform.Id }, platform);
}
Use DTOs (Data Transfer Objects)
Don't expose domain models directly; use DTOs to control what data is sent/received:
// Domain Model (internal)
public class Platform
{
public int Id { get; set; }
public string Name { get; set; }
public string InternalCode { get; set; } // Don't expose
public DateTime CreatedAt { get; set; }
public ICollection<Command> Commands { get; set; }
}
// DTO (external)
public class PlatformDto
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime CreatedAt { get; set; }
}
Use Dependency Injection
ASP.NET Core's built-in DI container makes it easy to inject dependencies:
// In Program.cs
builder.Services.AddScoped<IPlatformRepository, PlatformRepository>();
builder.Services.AddScoped<IPlatformService, PlatformService>();
// In Controller
public class PlatformsController : ControllerBase
{
private readonly IPlatformService _service;
public PlatformsController(IPlatformService service)
{
_service = service;
}
}
Return Appropriate HTTP Status Codes
Use the correct status codes to communicate results:
[HttpGet("{id}")]
public async Task<ActionResult<PlatformDto>> GetPlatform(int id)
{
var platform = await _service.GetPlatformByIdAsync(id);
if (platform == null)
return NotFound(); // 404
return Ok(platform); // 200
}
[HttpPost]
public async Task<ActionResult<PlatformDto>> CreatePlatform(PlatformCreateDto dto)
{
var platform = await _service.CreatePlatformAsync(dto);
return CreatedAtAction( // 201 with Location header
nameof(GetPlatform),
new { id = platform.Id },
platform);
}
[HttpPut("{id}")]
public async Task<IActionResult> UpdatePlatform(int id, PlatformUpdateDto dto)
{
var success = await _service.UpdatePlatformAsync(id, dto);
if (!success)
return NotFound(); // 404
return NoContent(); // 204
}
MVC and Modern .NET
While ASP.NET Core fully supports traditional MVC with Razor views for web applications, it also embraces modern approaches:
- Minimal APIs: Lightweight alternative for simple endpoints without full MVC overhead
- Blazor: Component-based UI framework (Server or WebAssembly)
- Web APIs: Focused on data serving for SPAs, mobile apps, and microservices
Despite these alternatives, MVC remains relevant and widely used, particularly for:
- Complex web applications requiring full MVC features
- APIs with sophisticated routing and validation needs
- Projects where teams prefer the structure and patterns MVC provides