All articles

ASP.NET Core Controller Action Methods and Types of Action Results

Controllers, action methods, and every action result type in ASP.NET Core — View, Json, Content, status codes, redirects, and FileResult — each verified with the actual HTTP response.

0 · log in to like, save & follow Share on LinkedIn Share on X

Every request that reaches an ASP.NET Core MVC application ends in a controller action, and every action returns an action result — the object that tells the framework what response to write. This tutorial covers what makes a class a controller, what makes a method an action, and the action result types you will actually use, each verified with the real HTTP response it produces.

cover

Controllers

A controller is a class that handles requests for one resource or area. MVC discovers it by convention — the Controller suffix — and routing maps URLs onto it:

public class DemoController : Controller
{
    // actions go here
}

Two base classes matter:

  • Controller — for pages: adds View(), ViewData, TempData, and everything Razor needs.
  • ControllerBase — for APIs: everything except views. Pair it with [ApiController] for automatic model validation responses and body binding.

Action methods

Every public method on a controller is an action — reachable over HTTP — unless you opt it out with [NonAction]. An action's job: take input (via model binding), do the work, and return a result.

Return types you can declare:

  • A specific result (JsonResult, ContentResult) — precise, but locks the action to one response kind.
  • IActionResult — the common choice: one action can return Ok(...) in the happy path and NotFound() in another branch.
  • ActionResult<T> — best for APIs: documents the success type T for OpenAPI while still allowing status-code results.

The action results, verified

Each of these was executed; the response shown is what actually came back.

ViewResult — render a page

public IActionResult Index() => View(products);

Renders the Razor view for the action with the model — the everyday result for MVC pages.

JsonResult — return data

public JsonResult AsJson() => Json(new { id = 1, name = "Galaxy A15" });
GET /demo/asjson → 200, {"id":1,"name":"Galaxy A15"}

Serialized with System.Text.Json, camelCased by default.

ContentResult — raw text

public ContentResult AsText() => Content("plain text response", "text/plain");
GET /demo/astext → 200, Content-Type: text/plain

Status-code results

public IActionResult Missing() => NotFound();                                  // 404
public IActionResult Invalid() => BadRequest(new { error = "price must be positive" }); // 400 + body
public IActionResult NoBody() => NoContent();                                  // 204
public StatusCodeResult Teapot() => StatusCode(418);                           // anything else

Verified: 404, 400 with the JSON error body, 204, 418. Ok(value) (200), CreatedAtAction(...) (201 + Location header) round out the API set — see them in action in CRUD operations using ASP.NET Core.

RedirectResult family

public IActionResult Moved() => RedirectToAction("AsJson");
GET /demo/moved → 302 Location: /Demo/AsJson

RedirectToAction, RedirectToRoute, Redirect(url), and their Permanent variants (301) — use permanent redirects when a URL has moved for good, so search engines transfer ranking.

FileResult — downloads

public FileResult DownloadBytes()
{
    var bytes = Encoding.UTF8.GetBytes("ProductId,Name\n1,Galaxy A15\n");
    return File(bytes, "text/csv", "products.csv");
}
GET /demo/downloadbytes → Content-Type: text/csv
Content-Disposition: attachment; filename=products.csv

Byte arrays, streams, and physical files each have an overload — the full treatment is in returning a file using FileResult.

ActionResult<T> and async actions

For APIs, ActionResult<T> is the modern signature — it documents the success type for OpenAPI while every status-code helper still works:

[HttpGet("{id:int}")]
public async Task<ActionResult<Supplier>> Get(int id, CancellationToken ct)
{
    var supplier = await db.Suppliers.FindAsync([id], ct);
    return supplier is null ? NotFound() : supplier;   // implicit conversion to 200
}

Three things worth internalizing:

  • Returning the object converts to 200 OK implicitly; returning NotFound() short-circuits — one signature, both outcomes typed.
  • Any action that does I/O should be async Task<…> — database calls, HTTP calls, file reads. The thread goes back to the pool while the work runs; under load this is the difference between serving requests and queueing them.
  • Accepting a CancellationToken parameter (bound automatically from the request) lets aborted requests stop the query instead of running to completion for a client that already left.

PartialViewResult and view components

Pages composed from fragments use two more results:

public IActionResult Row(int id) => PartialView("_SupplierRow", Find(id));

PartialView renders a view without the layout — the classic target for AJAX that replaces one section of a page. View components (ViewComponent classes returning IViewComponentResult) are their richer sibling: a mini controller+view pair invokable from any layout or page, right for self-contained widgets like a notification bell or cart summary.

Writing a custom action result

Every result is just a class implementing IActionResult — one method. A CSV result makes the pattern concrete:

public class CsvResult(IEnumerable<string[]> rows, string fileName) : IActionResult
{
    public async Task ExecuteResultAsync(ActionContext context)
    {
        var response = context.HttpContext.Response;
        response.ContentType = "text/csv";
        response.Headers.ContentDisposition = $"attachment; filename={fileName}";
        foreach (var row in rows)
            await response.WriteAsync(string.Join(',', row) + "\n");
    }
}
public IActionResult Export() => new CsvResult(GetRows(), "suppliers.csv");

The framework calls ExecuteResultAsync when the action returns — your class owns the response from there. Most needs are covered by the built-ins, but the extension point keeps controllers declarative when you do need something bespoke (server-sent events, custom streaming, exotic formats).

Picking the right result

  • Page → View(). Data for JavaScript → Json() or, in APIs, just return the object.
  • Client mistake → BadRequest/NotFound/UnprocessableEntity, never a 200 with an error string — status codes are the API contract.
  • After a successful POST in MVC pages → redirect (RedirectToAction) so refresh doesn't resubmit the form (the POST-redirect-GET pattern).
  • Everything a browser downloads → a FileResult with a real content type and filename.

The complete runnable controller for this article is in the companion repo — every endpoint above works with dotnet run and curl.

Enjoyed this article? Get the best GeeksArray articles in your inbox — once a week, no spam, unsubscribe anytime.

Comments (0)

Log in to join the conversation.

No comments yet — be the first to share your thoughts.