Controller Basics — Naming, Inheritance and DI

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
Note: Controllers are not singletons — a new instance is created for every HTTP request. This means instance fields (not injected services) are reset on every request and cannot be used to share state between requests. Injected services follow their registered lifetimes (Scoped services live for the request, Singleton services live for the application). This per-request instantiation model is why constructor injection is safe for Scoped services in controllers — each request gets its own controller instance with its own Scoped service instances.
Tip: Use C# 12 primary constructors for concise controller definitions when you have 1–3 dependencies. 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.
Warning: Do not mark controller classes or action methods as 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.

🧠 Test Yourself

A controller is created per HTTP request. Two simultaneous requests hit PostsController.Index(). Do they share the same controller instance?