A controller is a C# class that handles HTTP requests. ASP.NET Core discovers controllers by convention — any class whose name ends in Controller and inherits from Controller (or ControllerBase) is treated as a controller. The class name determines the route prefix: PostsController responds to /posts/*. The framework instantiates controllers per request using the DI container, injecting all registered services through the constructor. Understanding this activation model is the foundation for building every controller in this series.
Controller Discovery and Activation
// ── Controller naming convention ──────────────────────────────────────────
// Class name = {ControllerName}Controller
// "Controller" suffix is stripped for routing
// PostsController → route prefix "posts"
// HomeController → route prefix "home"
// AdminController → route prefix "admin"
// ── Basic MVC controller ──────────────────────────────────────────────────
public class PostsController : Controller // inherit Controller for MVC
{
private readonly IPostService _service;
private readonly ILogger<PostsController> _logger;
// DI: framework calls this constructor, injecting registered services
public PostsController(
IPostService service,
ILogger<PostsController> logger)
{
_service = service;
_logger = logger;
}
// GET /posts
public async Task<IActionResult> Index(int page = 1)
{
_logger.LogInformation("Listing posts, page {Page}", page);
var posts = await _service.GetPublishedAsync(page, 10);
return View(new PostIndexViewModel(posts, page));
}
// GET /posts/details/42
public async Task<IActionResult> Details(int id)
{
var post = await _service.GetByIdAsync(id);
if (post is null) return NotFound();
return View(new PostDetailsViewModel(post));
}
}
// ── What Controller base class provides ───────────────────────────────────
// this.View(model) — create ViewResult
// this.PartialView(name, model) — create PartialViewResult
// this.RedirectToAction(...) — create RedirectToActionResult
// this.NotFound() — create 404 result
// this.BadRequest() — create 400 result
// this.Ok(data) — create 200 result
// this.HttpContext — the current request context
// this.User — the current ClaimsPrincipal
// this.ModelState — model binding validation state
// this.TempData — cross-redirect data store
// this.ViewBag — dynamic per-view data bag
// this.ViewData — dictionary-based per-view data
public class PostsController(IPostService service, ILogger<PostsController> logger) : Controller eliminates the field declarations and assignment boilerplate. The injected parameters are available throughout all action methods. This is the pattern used in modern ASP.NET Core code examples throughout this series.static. MVC requires instantiating the controller per request to inject dependencies and set request-context properties (HttpContext, User, ModelState). A static controller cannot receive constructor injection, cannot access this.HttpContext, and will cause a runtime exception when the framework tries to activate it. Controllers must be non-static, non-abstract instance classes.Multiple Controllers and Shared Concerns
// ── HomeController — handles the root site pages ──────────────────────────
public class HomeController(ILogger<HomeController> logger) : Controller
{
public IActionResult Index() => View();
public IActionResult About() => View();
public IActionResult Contact() => View();
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
=> View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
// ── Base controller for shared behaviour across all controllers ──────────
// Extract common logic into a base class
public abstract class BaseController : Controller
{
protected string CurrentUserId
=> User.FindFirstValue(ClaimTypes.NameIdentifier) ?? string.Empty;
protected void AddSuccessMessage(string message)
=> TempData["SuccessMessage"] = message;
protected void AddErrorMessage(string message)
=> TempData["ErrorMessage"] = message;
}
// PostsController now inherits shared helpers
public class PostsController(IPostService service) : BaseController
{
public async Task<IActionResult> Create(CreatePostRequest request)
{
await service.CreateAsync(request, CurrentUserId);
AddSuccessMessage("Post created successfully!");
return RedirectToAction(nameof(Index));
}
}
Common Mistakes
Mistake 1 — Storing request-specific state in controller instance fields
❌ Wrong — instance field is reset every request; cannot share state:
public class PostsController : Controller
{
private List<Post> _cachedPosts = new(); // reset each request — useless!
✅ Correct — use Singleton services for shared state; use Scoped services for per-request state.
Mistake 2 — Inheriting ControllerBase in an MVC controller
❌ Wrong — View() and TempData unavailable; view rendering fails at runtime.
✅ Correct — inherit Controller for MVC; ControllerBase for Web API only.