Clients depend on correct status codes and response shapes; this is your API contract.
What a handler returns becomes the response:
- A
stringis written astext/plainwith 200. - Any other object (record, list, anonymous type) is serialized to JSON with 200.
- An
IResulttakes full control: status code, headers and body.
The Results and TypedResults static classes create IResults: Ok(value), Created(uri, value), NoContent(), BadRequest(error), NotFound(), Conflict(), Problem(...), ValidationProblem(errors), Unauthorized(), Forbid(), Redirect(url), File(bytes, contentType), Stream(...), StatusCode(429).
Prefer TypedResults: they return concrete types like Ok<Todo>, so the handler's return type documents its outcomes and OpenAPI can describe them automatically. When a handler can produce several outcomes, declare a union return type:
static Results<Ok<Todo>, NotFound> Get(int id) => ...Each branch returns a different TypedResult, and the compiler checks that only the declared results are used.
Async handlers return Task<T> and work the same way. Use proper semantics: 201 with a Location header for creation, 204 for successful deletes without a body, 404 for missing resources, 400/422 for invalid input, 409 for conflicts.
JSON options (naming policy, enum as strings) are configured with builder.Services.ConfigureHttpJsonOptions(o => ...).
Example
using Microsoft.AspNetCore.Http.HttpResults;
using System.Text.Json.Serialization;
var builder = WebApplication.CreateBuilder(args);
builder.Services.ConfigureHttpJsonOptions(o =>
o.SerializerOptions.Converters.Add(new JsonStringEnumConverter()));
var app = builder.Build();
var todos = new List<Todo> { new(1, "Learn results", Status.Open) };
app.MapGet("/todos/{id}", Results<Ok<Todo>, NotFound> (int id) =>
todos.Find(t => t.Id == id) is { } todo ? TypedResults.Ok(todo) : TypedResults.NotFound());
app.MapPost("/todos", Created<Todo> (Todo todo) =>
{
todos.Add(todo);
return TypedResults.Created($"/todos/{todo.Id}", todo);
});
app.MapGet("/ping", () => "pong"); // text/plain
app.Run();
// GET /todos/1 -> 200 {"id":1,"title":"Learn results","status":"Open"}
// GET /todos/9 -> 404
enum Status { Open, Done }
record Todo(int Id, string Title, Status Status);Remember
- Objects become JSON, strings become text/plain
- Return IResult for status codes and headers
- TypedResults + Results<A, B> make outcomes explicit and documentable
- Use the right status code for each outcome