Introduction to ASP.NET Core MVC
ASP.NET Core MVC is a rich, open-source framework for building web applications and APIs using the Model-View-Controller architectural pattern. The MVC pattern separates an application into three main components, promoting a clear separation of concerns that makes applications more maintainable and testable.
MVC Architecture Overview
Model
- Represents the application’s data and business logic
- Responsible for data validation and enforcing business rules
- Typically interacts with data storage (databases)
- Independent of the UI
- Examples: Entity classes, data access code, business services
View
- Handles the UI presentation layer
- Renders the model data for display
- Uses Razor syntax (.cshtml files)
- Should contain minimal logic
- Stored in the
/Views folder by default
Controller
- Handles user interactions and input
- Processes incoming requests
- Works with model data
- Selects appropriate views to render
- Stored in the
/Controllers folder by default - Entry point for handling HTTP requests
Project Structure
/YourProject
├── /Controllers # Controller classes
├── /Models # Model classes and data context
├── /Views # View templates
│ ├── /Home # Views for HomeController
│ ├── /Shared # Shared views (layouts, partials)
│ └── _ViewImports.cshtml # Import directives for views
│ └── _ViewStart.cshtml # Common view initialization
├── /wwwroot # Static files (CSS, JS, images)
│ ├── /css
│ ├── /js
│ └── /images
├── Program.cs # Application entry point and startup
├── appsettings.json # Configuration settings
└── YourProject.csproj # Project file
Controller Basics
Creating a Controller
using Microsoft.AspNetCore.Mvc;
namespace YourApp.Controllers
{
public class HomeController : Controller
{
// GET: /Home/
public IActionResult Index()
{
return View();
}
// GET: /Home/About
public IActionResult About()
{
ViewData["Message"] = "Your application description page.";
return View();
}
}
}
Controller Action Return Types
| Return Type | Description |
|---|
ViewResult | Renders a view to the response |
PartialViewResult | Renders a partial view |
RedirectResult | Redirects to a URL |
RedirectToActionResult | Redirects to another action |
JsonResult | Returns a JSON-formatted response |
ContentResult | Returns a text/plain response |
FileResult | Returns a file |
StatusCodeResult | Returns a specific HTTP status code |
Action Method Helper Methods
// Return a view
return View(); // Uses action name as view name
return View("ViewName"); // Specify view name
return View(model); // Pass a model to the view
return View("ViewName", model); // Specify view and model
// Redirection
return RedirectToAction("Index");
return RedirectToAction("Index", "Home");
return RedirectToAction("Index", new { id = 123 });
return Redirect("/Home/Index");
return RedirectToRoute("routename");
// API responses
return Json(new { name = "John", age = 30 });
return Content("Plain text response");
return File("/path/to/file.pdf", "application/pdf");
return NotFound(); // 404
return BadRequest(); // 400
return Unauthorized(); // 401
return StatusCode(StatusCodes.Status500InternalServerError);
Route Configuration
Convention-based Routing
// In Program.cs
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
Attribute Routing
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet] // GET: /api/products
public IActionResult GetAll() { ... }
[HttpGet("{id}")] // GET: /api/products/5
public IActionResult GetById(int id) { ... }
[HttpPost] // POST: /api/products
public IActionResult Create([FromBody] Product product) { ... }
[HttpPut("{id}")] // PUT: /api/products/5
public IActionResult Update(int id, [FromBody] Product product) { ... }
[HttpDelete("{id}")] // DELETE: /api/products/5
public IActionResult Delete(int id) { ... }
}
Route Constraints
// Numeric constraint
app.MapControllerRoute(
name: "product",
pattern: "Product/{id:int}",
defaults: new { controller = "Product", action = "Details" });
// Common constraints:
// {id:int} - Integer values
// {id:guid} - GUID values
// {name:alpha} - Alphabetic characters
// {file:regex(^[a-z0-9]+\.(jpg|png)$)} - Regular expression
Model Basics
Creating a Simple Model
public class Movie
{
public int Id { get; set; }
[Required]
[StringLength(100)]
public string Title { get; set; }
[DataType(DataType.Date)]
[Display(Name = "Release Date")]
public DateTime ReleaseDate { get; set; }
[Range(1, 10)]
public decimal Rating { get; set; }
[Required]
[StringLength(500)]
public string Description { get; set; }
}
Data Annotations
| Annotation | Description |
|---|
[Required] | Specifies that the property must have a value |
[StringLength(max)] | Specifies max length of a string |
[Range(min, max)] | Specifies a numeric range |
[RegularExpression(pattern)] | Validates against a regex pattern |
[EmailAddress] | Validates email format |
[Phone] | Validates phone number format |
[Url] | Validates URL format |
[DataType(DataType.X)] | Specifies the data type (Date, Password, etc) |
[Display(Name = "X")] | Sets display name for UI |
[Compare("PropertyName")] | Compares with another property |
Database Context with Entity Framework Core
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<Movie> Movies { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Define relationships, constraints, etc.
modelBuilder.Entity<Movie>()
.HasIndex(m => m.Title);
}
}
// In Program.cs
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
Model Binding
// Binding from route values, query strings, or form data
public IActionResult Edit(int id)
// Binding from request body (for APIs)
public IActionResult Create([FromBody] Movie movie)
// Binding from specific sources
public IActionResult Update(
[FromRoute] int id,
[FromBody] Movie movie,
[FromQuery] bool validate,
[FromHeader(Name = "Authorization")] string authorization)
View Basics
Creating a Simple View
@model YourApp.Models.Movie
@{
ViewData["Title"] = "Movie Details";
}
<h2>@Model.Title</h2>
<dl>
<dt>@Html.DisplayNameFor(model => model.ReleaseDate)</dt>
<dd>@Html.DisplayFor(model => model.ReleaseDate)</dd>
<dt>@Html.DisplayNameFor(model => model.Rating)</dt>
<dd>@Html.DisplayFor(model => model.Rating)</dd>
<dt>@Html.DisplayNameFor(model => model.Description)</dt>
<dd>@Html.DisplayFor(model => model.Description)</dd>
</dl>
Razor Syntax Basics
@* This is a comment *@
@* Variables and code blocks *@
@{
var message = "Hello, World!";
var currentTime = DateTime.Now;
}
@* Displaying values *@
<p>@message</p>
<p>The time is: @currentTime</p>
@* Conditional rendering *@
@if (Model.Rating > 8)
{
<p>Highly rated!</p>
}
else
{
<p>Average rating</p>
}
@* Loops *@
<ul>
@foreach (var item in Model.Items)
{
<li>@item.Name</li>
}
</ul>
@* Switch statements *@
@switch (Model.Status)
{
case "A":
<p>Active</p>
break;
case "I":
<p>Inactive</p>
break;
default:
<p>Unknown</p>
break;
}
Layouts and Partial Views
@* _Layout.cshtml *@
<!DOCTYPE html>
<html>
<head>
<title>@ViewData["Title"] - My App</title>
<link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
<header>
<partial name="_NavbarPartial" />
</header>
<div class="container">
@RenderBody()
</div>
<footer>
<p>© @DateTime.Now.Year - My Application</p>
</footer>
@RenderSection("Scripts", required: false)
</body>
</html>
@* _NavbarPartial.cshtml *@
<nav>
<ul>
<li><a asp-controller="Home" asp-action="Index">Home</a></li>
<li><a asp-controller="Movies" asp-action="Index">Movies</a></li>
</ul>
</nav>
@* View using layout (often in _ViewStart.cshtml) *@
@{
Layout = "_Layout";
}
Tag Helpers
@* Navigation links *@
<a asp-controller="Home" asp-action="Index">Home</a>
<a asp-controller="Movies" asp-action="Details" asp-route-id="@Model.Id">Movie Details</a>
@* Form tag helpers *@
<form asp-controller="Account" asp-action="Login" method="post">
<div class="form-group">
<label asp-for="Email"></label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Password"></label>
<input asp-for="Password" class="form-control" />
<span asp-validation-for="Password" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Login</button>
</form>
@* Environment tag helper *@
<environment include="Development">
<script src="~/js/site.js"></script>
</environment>
<environment exclude="Development">
<script src="~/js/site.min.js" asp-append-version="true"></script>
</environment>
ViewData, ViewBag, and TempData
// In Controller
public IActionResult Index()
{
// ViewData (dictionary)
ViewData["Title"] = "Home Page";
ViewData["CurrentTime"] = DateTime.Now;
// ViewBag (dynamic)
ViewBag.Message = "Welcome to my app";
ViewBag.User = new { Name = "John", Role = "Admin" };
// TempData (persists for the next request)
TempData["Notification"] = "Your changes were saved!";
return View();
}
@* In View *@
<h1>@ViewData["Title"]</h1>
<p>Current time: @ViewData["CurrentTime"]</p>
<h2>@ViewBag.Message</h2>
<p>User: @ViewBag.User.Name (@ViewBag.User.Role)</p>
@if (TempData["Notification"] != null)
{
<div class="alert alert-success">
@TempData["Notification"]
</div>
}
Form Handling and Validation
Form Submission and Model Binding
// GET: /Movies/Create
public IActionResult Create()
{
return View();
}
// POST: /Movies/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(Movie movie)
{
if (ModelState.IsValid)
{
_context.Add(movie);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(movie);
}
Client-Side Validation
@* Add these to layout or view *@
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
@* In a form *@
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Title" class="control-label"></label>
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ReleaseDate" class="control-label"></label>
<input asp-for="ReleaseDate" class="form-control" />
<span asp-validation-for="ReleaseDate" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
Custom Validation
// Custom validation attribute
public class FutureDateAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
DateTime date = (DateTime)value;
if (date < DateTime.Today)
{
return new ValidationResult("Date must be in the future!");
}
return ValidationResult.Success;
}
}
// Using the custom attribute in a model
public class Event
{
public int Id { get; set; }
[Required]
public string Name { get; set; }
[FutureDate]
[DataType(DataType.Date)]
public DateTime EventDate { get; set; }
}
Dependency Injection
Registering Services
// In Program.cs
// Built-in service registration
builder.Services.AddControllersWithViews();
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// Various service lifetimes
builder.Services.AddTransient<ITransientService, TransientService>();
builder.Services.AddScoped<IScopedService, ScopedService>();
builder.Services.AddSingleton<ISingletonService, SingletonService>();
// Register a custom service
builder.Services.AddScoped<IMovieService, MovieService>();
Using Injected Services
public class MoviesController : Controller
{
private readonly IMovieService _movieService;
private readonly ILogger<MoviesController> _logger;
// Constructor injection
public MoviesController(
IMovieService movieService,
ILogger<MoviesController> logger)
{
_movieService = movieService;
_logger = logger;
}
public async Task<IActionResult> Index()
{
_logger.LogInformation("Getting all movies");
var movies = await _movieService.GetAllMoviesAsync();
return View(movies);
}
}
Configuration
Reading Configuration
// In Program.cs
var builder = WebApplication.CreateBuilder(args);
// Configuration is already set up by the builder
// Reading configuration in a controller
public class HomeController : Controller
{
private readonly IConfiguration _configuration;
public HomeController(IConfiguration configuration)
{
_configuration = configuration;
}
public IActionResult Index()
{
// Reading values
var apiKey = _configuration["ApiKey"];
var timeoutSeconds = _configuration.GetValue<int>("Settings:TimeoutSeconds");
var connectionString = _configuration.GetConnectionString("DefaultConnection");
return View();
}
}
Configuration in appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=MyDb;Trusted_Connection=True;"
},
"ApiKey": "your-api-key-here",
"Settings": {
"TimeoutSeconds": 30,
"EnableFeature": true
}
}
Options Pattern
// Options class
public class MovieApiOptions
{
public string BaseUrl { get; set; }
public string ApiKey { get; set; }
public int TimeoutSeconds { get; set; }
}
// In Program.cs
builder.Services.Configure<MovieApiOptions>(
builder.Configuration.GetSection("MovieApi"));
// In a service
public class MovieService : IMovieService
{
private readonly MovieApiOptions _options;
public MovieService(IOptions<MovieApiOptions> options)
{
_options = options.Value;
}
public async Task<Movie> GetMovieAsync(int id)
{
// Use _options.BaseUrl, _options.ApiKey, etc.
}
}
Middleware and Error Handling
Configuring Middleware
// In Program.cs
var app = builder.Build();
// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
Global Error Handling
// Error controller
public class ErrorController : Controller
{
[Route("Error/{statusCode}")]
public IActionResult HttpStatusCodeHandler(int statusCode)
{
var statusCodeResult =
HttpContext.Features.Get<IStatusCodeReExecuteFeature>();
switch (statusCode)
{
case 404:
ViewBag.ErrorMessage = "Sorry, the page you requested could not be found";
ViewBag.Path = statusCodeResult.OriginalPath;
break;
case 500:
ViewBag.ErrorMessage = "Sorry, something went wrong on the server";
break;
}
return View("Error");
}
[Route("Error")]
[AllowAnonymous]
public IActionResult Error()
{
var exceptionDetails =
HttpContext.Features.Get<IExceptionHandlerPathFeature>();
ViewBag.ExceptionPath = exceptionDetails.Path;
ViewBag.ExceptionMessage = exceptionDetails.Error.Message;
return View("Error");
}
}
Custom Middleware
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
_logger.LogInformation(
"Request {Method} {Path}",
context.Request.Method,
context.Request.Path);
// Call the next middleware in the pipeline
await _next(context);
_logger.LogInformation(
"Response {StatusCode}",
context.Response.StatusCode);
}
}
// Extension method for cleaner registration
public static class RequestLoggingMiddlewareExtensions
{
public static IApplicationBuilder UseRequestLogging(this IApplicationBuilder app)
{
return app.UseMiddleware<RequestLoggingMiddleware>();
}
}
// In Program.cs
app.UseRequestLogging();
Areas
Setting Up Areas
// In Program.cs
app.MapControllerRoute(
name: "areaRoute",
pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
Creating Area Structure
/Areas
/Admin
/Controllers
HomeController.cs
/Models
/Views
/Home
Index.cshtml
/User
/Controllers
/Models
/Views
Area Controller
[Area("Admin")]
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
}
Area Navigation
<a asp-area="Admin" asp-controller="Home" asp-action="Index">Admin Home</a>
Security
Authentication and Authorization Setup
// In Program.cs
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Account/Login";
options.AccessDeniedPath = "/Account/AccessDenied";
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy =>
policy.RequireRole("Admin"));
options.AddPolicy("EmployeeOnly", policy =>
policy.RequireRole("Employee", "Manager", "Admin"));
});
// In the pipeline
app.UseAuthentication();
app.UseAuthorization();
Applying Authorization
// Controller or action level
[Authorize]
public class AccountController : Controller
[Authorize(Roles = "Admin")]
public IActionResult AdminPanel()
[Authorize(Policy = "EmployeeOnly")]
public IActionResult EmployeeArea()
[AllowAnonymous]
public IActionResult Login()
Cross-Site Request Forgery (CSRF) Protection
// In controller action
[ValidateAntiForgeryToken]
[HttpPost]
public async Task<IActionResult> Edit(int id, Movie movie)
<!-- In form -->
<form asp-action="Edit">
@Html.AntiForgeryToken()
<!-- form fields -->
</form>
<!-- Or with tag helpers -->
<form asp-action="Edit" method="post">
<!-- form fields (token included automatically) -->
</form>
Resources
Official Documentation
Community Resources
Tools