All articles

Return Different Types of Content from ASP.NET Core Action Results

Serve HTML, JSON, plain text, XML, files, and bare status codes from ASP.NET Core actions — with content negotiation, verified response headers, and guidance on choosing.

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

One controller action can serve HTML, JSON, plain text, XML, files, or nothing but a status code — the action result decides what goes on the wire. This tutorial returns each content type from an ASP.NET Core action and shows the actual response headers and bodies, plus how content negotiation picks a format for API results.

cover

If you want the full map of action result classes first, start with controller action methods and types of action results — this post focuses on the content each one produces.

HTML — ViewResult

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

Razor renders the view to text/html. This is the default for MVC pages; everything else below is for data, files, and machine clients.

JSON

public JsonResult AsJson() => Json(new { id = 1, name = "Galaxy A15" });
200 OK, Content-Type: application/json; charset=utf-8
{"id":1,"name":"Galaxy A15"}

In an [ApiController] you rarely call Json() — return the object (or Ok(obj)) and the framework serializes it. Property names come out camelCased by System.Text.Json defaults.

Plain text — ContentResult

public ContentResult AsText() => Content("plain text response", "text/plain");
200 OK, Content-Type: text/plain
plain text response

Content() takes any content type — handy for robots.txt-style endpoints, health checks, or hand-built XML/CSV when a full serializer is overkill.

XML

JSON is the default; XML is opt-in. Enable the formatters and clients can ask for it:

builder.Services.AddControllers()
    .AddXmlSerializerFormatters();

With that, an API action returning ActionResult<Product> serves JSON for Accept: application/json and XML for Accept: application/xml — that's content negotiation: the client's Accept header picks among the formatters you registered. No per-action code.

Files — FileResult

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

The browser downloads products.csv instead of rendering. Streams and on-disk files have their own overloads — details in returning a file using FileResult, and a real spreadsheet example in export SQL data to Excel.

No content at all — status codes

Sometimes the status line is the content:

public IActionResult NoBody() => NoContent();     // 204 — success, nothing to say (deletes)
public IActionResult Missing() => NotFound();     // 404
public IActionResult Invalid() =>
    BadRequest(new { error = "price must be positive" });  // 400 + JSON problem body

All three verified: 204 with an empty body, 404, and 400 carrying the JSON error. For validation failures, [ApiController] produces a standardized application/problem+json body automatically — see model validation with DataAnnotations.

Redirects — Location as the content

public IActionResult Moved() => RedirectToAction("AsJson");
302 Found, Location: /Demo/AsJson

The body is empty; the Location header tells the client where to go. Use the Permanent variants (301) for URLs that moved for good.

Declaring what an action produces

Content negotiation works better when actions declare their formats — OpenAPI documents them and the framework can short-circuit unacceptable requests:

[HttpGet("{id:int}")]
[Produces("application/json", "application/xml")]
[ProducesResponseType(typeof(Supplier), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Supplier>> Get(int id) { … }

[Produces] pins the formats (a request demanding Accept: text/html gets 406 Not Acceptable instead of a best-effort guess); [ProducesResponseType] feeds the OpenAPI document that tools like Scalar render — see getting started with Web API.

Shaping the JSON itself

The content type says JSON; JsonOptions says which JSON:

builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; // default
    options.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
    options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
});

The enum converter is the one most APIs want — "status": "Active" instead of "status": 1, so the payload stays meaningful when the enum gains members. Per-property control lives on the model: [JsonPropertyName("sku")], [JsonIgnore].

Errors as content: ProblemDetails

Errors deserve a content type too. ASP.NET Core standardizes on RFC 7807 problem details (application/problem+json) — the shape [ApiController] already returns for validation failures:

return Problem(
    title: "Supplier is archived",
    detail: $"Supplier {id} was archived and cannot be modified.",
    statusCode: StatusCodes.Status409Conflict);

For unhandled exceptions, AddProblemDetails() plus UseExceptionHandler() turns 500s into the same shape — clients then parse every error the same way instead of scraping HTML error pages.

Choosing in practice

You're returning Use
A page View()
Data for JavaScript / API clients return the object (ActionResult<T>) or Json()
A quick string with a specific MIME type Content(text, contentType)
JSON and XML from one API formatters + content negotiation
A download File(...) with real content type + filename
Just an outcome NoContent(), NotFound(), BadRequest()

One rule above all: never return 200 OK with an error message in the body — the status code is the contract that clients, proxies, and monitoring all rely on.

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.