Results & Response Types
Master IResult, TypedResults, and proper HTTP responses
Results & Response Types in ASP.NET
Master the art of returning responses in ASP.NET with free flashcards and spaced repetition practice. This lesson covers action results, status codes, content negotiation, and custom response typesβessential concepts for building professional Web APIs with .NET 10. Understanding how to properly shape and return responses is fundamental to creating maintainable, RESTful APIs that clients can rely on.
Welcome π
Welcome to the world of ASP.NET response handling! π» When building Web APIs, how you return data is just as important as what you return. In this lesson, you'll learn about the various result types available in ASP.NET, from simple status codes to complex content negotiation strategies. Whether you're returning JSON, XML, files, or custom formats, ASP.NET provides a rich set of tools to handle every scenario.
π― By the end of this lesson, you'll confidently choose the right response type for every situation, understand HTTP status codes deeply, and know how to customize responses to meet your API's needs.
Core Concepts
Understanding Action Results π¬
In ASP.NET, action results are the return types from controller actions. The IActionResult interface is the foundation of ASP.NET's response system. Think of action results as containers that wrap your data along with metadata about how the response should be formatted and delivered.
| Result Type | HTTP Status | Use Case |
|---|---|---|
OkResult | 200 | Success without body |
OkObjectResult | 200 | Success with data |
CreatedResult | 201 | Resource created |
NoContentResult | 204 | Success, no content to return |
BadRequestResult | 400 | Invalid client request |
NotFoundResult | 404 | Resource not found |
UnauthorizedResult | 401 | Authentication required |
π‘ Tip: Use ActionResult<T> as your return type to get strong typing benefits while maintaining flexibility:
public ActionResult<Product> GetProduct(int id)
{
var product = _repository.GetById(id);
if (product == null)
return NotFound();
return Ok(product);
}
The Result Pattern Hierarchy ποΈ
ASP.NET's result types follow a clear hierarchy:
IActionResult
β
βββββββββββββββΌββββββββββββββ
β β β
StatusCodeResult β ObjectResult
β β β
ββββββ΄βββββ FileResult ββββββ΄βββββ
β β β β β
Ok NoContent PhysicalFile JsonResult XmlResult
NotFound Created β
BadRequest Accepted ContentResult
StatusCodeResult types return specific HTTP status codes without a body. ObjectResult types serialize data into the response body using content negotiation.
Status Codes: The HTTP Language π‘
HTTP status codes are the universal language between client and server. Understanding them is crucial for API design:
2xx Success β
- 200 OK: Request succeeded, here's your data
- 201 Created: New resource created, location in header
- 204 No Content: Success, but nothing to return (common for DELETE)
4xx Client Errors β οΈ
- 400 Bad Request: Malformed request or validation failure
- 401 Unauthorized: Authentication required (misleading name!)
- 403 Forbidden: Authenticated but lacks permission
- 404 Not Found: Resource doesn't exist
- 409 Conflict: Request conflicts with current state
5xx Server Errors π₯
- 500 Internal Server Error: Something went wrong server-side
- 503 Service Unavailable: Server temporarily can't handle request
π§ Memory Device: Think "4xx = Fault of client, 5xx = Fault of server" (F comes before S, 4 before 5)
Helper Methods in ControllerBase π οΈ
The ControllerBase class provides convenient helper methods that return appropriate result types:
| Helper Method | Returns | Status Code |
|---|---|---|
Ok() | OkResult | 200 |
Ok(value) | OkObjectResult | 200 |
Created(uri, value) | CreatedResult | 201 |
CreatedAtAction() | CreatedAtActionResult | 201 |
NoContent() | NoContentResult | 204 |
BadRequest() | BadRequestResult | 400 |
NotFound() | NotFoundResult | 404 |
StatusCode(code) | StatusCodeResult | Custom |
Content Negotiation π€
Content negotiation is ASP.NET's way of serving the same data in different formats based on what the client requests. The client sends an Accept header, and ASP.NET automatically serializes your data accordingly.
CLIENT REQUEST:
GET /api/products/1
Accept: application/json
β
ββββββββββββββββ
β ASP.NET β Checks Accept header
β Controller β Finds JSON formatter
ββββββββββββββββ Serializes object
β
SERVER RESPONSE:
Content-Type: application/json
{"id": 1, "name": "Widget"}
By default, ASP.NET supports:
- application/json (default, uses System.Text.Json)
- text/json
- application/xml (requires configuration)
- text/xml (requires configuration)
π‘ Tip: You can add custom formatters to support additional media types like CSV, Protocol Buffers, or MessagePack.
Typed Results vs IActionResult π―
.NET 7+ introduced typed results through the Results static class, offering a more functional approach:
// Traditional approach
public IActionResult GetProduct(int id)
{
return Ok(new Product { Id = id });
}
// Typed results approach (minimal APIs)
app.MapGet("/products/{id}", (int id) =>
Results.Ok(new Product { Id = id }));
ActionResult
public ActionResult<IEnumerable<Product>> GetAll()
{
// Implicitly converts to OkObjectResult
return _products;
// Or explicitly return result types
// return Ok(_products);
}
π€ Did you know? The ActionResult<T> type uses implicit conversion operators, so you can return either the raw data type T or any IActionResult implementation!
Custom Response Types π¨
Sometimes you need more control over the response. ASP.NET provides several ways to customize:
1. Custom Status Codes:
return StatusCode(418, "I'm a teapot"); // RFC 2324 Easter egg!
2. Custom Headers:
Response.Headers.Add("X-Custom-Header", "value");
return Ok(data);
3. File Results:
// Physical file
return PhysicalFile("/path/to/file.pdf", "application/pdf");
// Byte array
return File(bytes, "image/png", "photo.png");
// Stream
return File(stream, "application/octet-stream");
4. Problem Details (RFC 7807):
return Problem(
title: "Resource not found",
detail: "Product with ID 123 does not exist",
statusCode: 404,
instance: HttpContext.Request.Path
);
Validation and Model Binding π
When accepting data, model binding converts request data into .NET objects, and validation ensures data integrity:
public ActionResult<Product> Create([FromBody] ProductDto dto)
{
// ModelState automatically populated by validation attributes
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var product = _mapper.Map<Product>(dto);
_repository.Add(product);
return CreatedAtAction(
nameof(GetById),
new { id = product.Id },
product
);
}
The ValidationProblem() helper returns standardized validation errors:
if (!ModelState.IsValid)
{
return ValidationProblem(ModelState);
}
This returns a 422 Unprocessable Entity with structured error details.
Asynchronous Results β‘
Modern APIs should return Task<ActionResult<T>> for async operations:
public async Task<ActionResult<Product>> GetByIdAsync(int id)
{
var product = await _repository.GetByIdAsync(id);
if (product == null)
return NotFound();
return Ok(product);
}
π Real-world analogy: Think of async results like ordering food at a restaurant. Instead of blocking the waiter (thread) while the kitchen prepares your meal, the waiter takes your order and moves on to other customers. When your food is ready, it's delivered to youβjust like await returns control when the operation completes.
Examples
Example 1: RESTful CRUD Operations
Here's a complete controller demonstrating proper response types for CRUD operations:
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductRepository _repository;
public ProductsController(IProductRepository repository)
{
_repository = repository;
}
// GET: api/products
[HttpGet]
public async Task<ActionResult<IEnumerable<Product>>> GetAll()
{
var products = await _repository.GetAllAsync();
return Ok(products); // 200 with array
}
// GET: api/products/5
[HttpGet("{id}")]
public async Task<ActionResult<Product>> GetById(int id)
{
var product = await _repository.GetByIdAsync(id);
if (product == null)
return NotFound(); // 404
return Ok(product); // 200 with object
}
// POST: api/products
[HttpPost]
public async Task<ActionResult<Product>> Create([FromBody] ProductDto dto)
{
if (!ModelState.IsValid)
return BadRequest(ModelState); // 400 with errors
var product = new Product
{
Name = dto.Name,
Price = dto.Price
};
await _repository.AddAsync(product);
// 201 with Location header pointing to GetById
return CreatedAtAction(
nameof(GetById),
new { id = product.Id },
product
);
}
// PUT: api/products/5
[HttpPut("{id}")]
public async Task<IActionResult> Update(int id, [FromBody] ProductDto dto)
{
var existing = await _repository.GetByIdAsync(id);
if (existing == null)
return NotFound(); // 404
existing.Name = dto.Name;
existing.Price = dto.Price;
await _repository.UpdateAsync(existing);
return NoContent(); // 204 (success, no body needed)
}
// DELETE: api/products/5
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id)
{
var product = await _repository.GetByIdAsync(id);
if (product == null)
return NotFound(); // 404
await _repository.DeleteAsync(id);
return NoContent(); // 204
}
}
Key points:
- GET operations return
200 OKwith data or404 Not Found - POST returns
201 CreatedwithLocationheader - PUT/DELETE return
204 No Contenton success - Validation errors return
400 Bad Request
Example 2: Custom Problem Details for Better Error Handling
Problem Details (RFC 7807) provides a standardized way to return error information:
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly IOrderService _orderService;
[HttpPost("{id}/ship")]
public async Task<ActionResult<Order>> ShipOrder(int id)
{
var order = await _orderService.GetByIdAsync(id);
if (order == null)
{
return Problem(
title: "Order not found",
detail: $"No order exists with ID {id}",
statusCode: StatusCodes.Status404NotFound,
instance: HttpContext.Request.Path
);
}
if (order.Status != OrderStatus.Paid)
{
return Problem(
title: "Cannot ship unpaid order",
detail: $"Order {id} has status '{order.Status}'. Only paid orders can be shipped.",
statusCode: StatusCodes.Status409Conflict,
instance: HttpContext.Request.Path,
type: "https://docs.myapi.com/errors/order-not-paid"
);
}
try
{
await _orderService.ShipAsync(id);
return Ok(order);
}
catch (ShippingException ex)
{
return Problem(
title: "Shipping failed",
detail: ex.Message,
statusCode: StatusCodes.Status500InternalServerError
);
}
}
}
Response example:
{
"type": "https://docs.myapi.com/errors/order-not-paid",
"title": "Cannot ship unpaid order",
"status": 409,
"detail": "Order 123 has status 'Pending'. Only paid orders can be shipped.",
"instance": "/api/orders/123/ship"
}
π‘ Tip: The type field should be a URI pointing to human-readable documentation about this error type.
Example 3: Content Negotiation and Multiple Formats
Supporting multiple response formats based on client preferences:
// Startup.cs or Program.cs
builder.Services.AddControllers(options =>
{
// Add XML formatter support
options.RespectBrowserAcceptHeader = true;
options.ReturnHttpNotAcceptable = true; // Return 406 if format not supported
})
.AddXmlSerializerFormatters() // Enable XML
.AddXmlDataContractSerializerFormatters();
// Controller
[ApiController]
[Route("api/[controller]")]
public class DataController : ControllerBase
{
// This endpoint supports both JSON and XML
[HttpGet]
[Produces("application/json", "application/xml")]
public ActionResult<Product> Get()
{
var product = new Product
{
Id = 1,
Name = "Widget",
Price = 19.99m
};
// ASP.NET automatically serializes based on Accept header
return Ok(product);
}
// Force JSON response regardless of Accept header
[HttpGet("json-only")]
[Produces("application/json")]
public ActionResult<Product> GetJsonOnly()
{
return Ok(new Product { Id = 2, Name = "Gadget" });
}
// Custom format based on query parameter
[HttpGet("custom")]
public IActionResult GetCustomFormat([FromQuery] string format = "json")
{
var data = new Product { Id = 3, Name = "Gizmo", Price = 29.99m };
return format.ToLower() switch
{
"json" => Ok(data),
"xml" => new ObjectResult(data)
{
ContentTypes = { "application/xml" }
},
"csv" => Content(
$"{data.Id},{data.Name},{data.Price}",
"text/csv"
),
_ => StatusCode(406, "Unsupported format")
};
}
}
Client requests:
GET /api/data
Accept: application/json
β Returns JSON
GET /api/data
Accept: application/xml
β Returns XML
GET /api/data
Accept: text/html
β Returns 406 Not Acceptable (if ReturnHttpNotAcceptable = true)
Example 4: File Downloads and Streaming
Returning files with appropriate content types:
[ApiController]
[Route("api/[controller]")]
public class FilesController : ControllerBase
{
private readonly IWebHostEnvironment _environment;
public FilesController(IWebHostEnvironment environment)
{
_environment = environment;
}
// Download a physical file
[HttpGet("download/{filename}")]
public IActionResult DownloadFile(string filename)
{
var filePath = Path.Combine(
_environment.ContentRootPath,
"Files",
filename
);
if (!System.IO.File.Exists(filePath))
return NotFound();
var contentType = filename.EndsWith(".pdf")
? "application/pdf"
: "application/octet-stream";
return PhysicalFile(filePath, contentType, filename);
}
// Stream generated content
[HttpGet("report")]
public async Task<IActionResult> GenerateReport()
{
var stream = new MemoryStream();
var writer = new StreamWriter(stream);
await writer.WriteLineAsync("Product Report");
await writer.WriteLineAsync("==============");
await writer.WriteLineAsync("Product 1: $19.99");
await writer.WriteLineAsync("Product 2: $29.99");
await writer.FlushAsync();
stream.Position = 0;
return File(
stream,
"text/plain",
"report.txt"
);
}
// Return byte array (e.g., generated image)
[HttpGet("image/{id}")]
public async Task<IActionResult> GetImage(int id)
{
byte[] imageBytes = await GenerateImageAsync(id);
if (imageBytes == null)
return NotFound();
return File(imageBytes, "image/png");
}
private async Task<byte[]> GenerateImageAsync(int id)
{
// Simulate image generation
await Task.Delay(100);
return new byte[] { 0x89, 0x50, 0x4E, 0x47 }; // PNG header
}
}
Download triggers:
FileResultautomatically setsContent-Disposition: attachment- Browser prompts user to save the file
- Third parameter (filename) determines default save name
Common Mistakes
β οΈ Mistake 1: Returning Wrong Status Codes
β Wrong:
[HttpPost]
public ActionResult<Product> Create(ProductDto dto)
{
var product = _repository.Add(dto);
return Ok(product); // Should be 201, not 200!
}
β Correct:
[HttpPost]
public ActionResult<Product> Create(ProductDto dto)
{
var product = _repository.Add(dto);
return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}
Why it matters: REST conventions specify 201 Created for resource creation, including a Location header with the new resource's URI.
β οΈ Mistake 2: Forgetting to Check ModelState
β Wrong:
[HttpPost]
public ActionResult<User> Register(RegisterDto dto)
{
// Validation attributes are ignored!
var user = _userService.Create(dto);
return Ok(user);
}
β Correct:
[HttpPost]
public ActionResult<User> Register(RegisterDto dto)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
var user = _userService.Create(dto);
return CreatedAtAction(nameof(GetById), new { id = user.Id }, user);
}
π‘ Better yet: Use [ApiController] attribute, which automatically validates and returns 400 for invalid models.
β οΈ Mistake 3: Not Using Async for I/O Operations
β Wrong:
public ActionResult<Product> GetById(int id)
{
var product = _repository.GetById(id); // Blocking call!
return product == null ? NotFound() : Ok(product);
}
β Correct:
public async Task<ActionResult<Product>> GetById(int id)
{
var product = await _repository.GetByIdAsync(id);
return product == null ? NotFound() : Ok(product);
}
Why it matters: Blocking calls tie up thread pool threads, reducing scalability. Async allows threads to handle other requests while waiting for I/O.
β οΈ Mistake 4: Exposing Internal Errors
β Wrong:
public ActionResult<Order> ProcessOrder(int id)
{
try
{
return Ok(_orderService.Process(id));
}
catch (Exception ex)
{
// Exposes internal details, stack traces!
return BadRequest(ex);
}
}
β Correct:
public ActionResult<Order> ProcessOrder(int id)
{
try
{
return Ok(_orderService.Process(id));
}
catch (InvalidOperationException ex)
{
return Problem(
title: "Order processing failed",
detail: "The order could not be processed. Please try again later.",
statusCode: 500
);
}
}
Why it matters: Exposing exception details is a security risk. Use proper logging and return sanitized error messages.
β οΈ Mistake 5: Ignoring Content Negotiation
β Wrong:
public IActionResult GetData()
{
var json = JsonSerializer.Serialize(data);
return Content(json, "application/json"); // Hardcoded JSON
}
β Correct:
public ActionResult<DataDto> GetData()
{
return Ok(data); // Let ASP.NET handle serialization
}
Why it matters: Ok(data) respects the client's Accept header and can return JSON, XML, or other configured formats automatically.
β οΈ Mistake 6: Not Setting Proper Cache Headers
β Wrong:
[HttpGet("{id}")]
public ActionResult<Product> GetProduct(int id)
{
var product = _repository.GetById(id);
return Ok(product); // No cache control
}
β Correct:
[HttpGet("{id}")]
[ResponseCache(Duration = 60, Location = ResponseCacheLocation.Any)]
public ActionResult<Product> GetProduct(int id)
{
var product = _repository.GetById(id);
return Ok(product);
}
Why it matters: Proper cache headers reduce server load and improve client performance.
Key Takeaways
β
Use ActionResult<T> for strongly-typed responses with flexibility
β Return appropriate HTTP status codes: 2xx for success, 4xx for client errors, 5xx for server errors
β
Use helper methods: Ok(), NotFound(), BadRequest(), Created(), etc.
β
Check ModelState.IsValid or use [ApiController] for automatic validation
β
Return Task<ActionResult<T>> for all async operations
β Use Problem Details for standardized error responses
β Let ASP.NET handle content negotiation instead of manual serialization
β
Use CreatedAtAction() for POST endpoints to include Location header
β
Return NoContent() (204) for successful PUT/DELETE operations
β Sanitize error messages to avoid exposing internal implementation details
π§ Remember: Results Represent Responses Respectfullyβchoose the right result type to properly communicate with your API clients!
π Further Study
- Microsoft Docs - Controller action return types: https://learn.microsoft.com/en-us/aspnet/core/web-api/action-return-types
- Problem Details RFC 7807: https://www.rfc-editor.org/rfc/rfc7807
- HTTP Status Codes Reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
π Quick Reference Card
| Operation | Success Code | Return Type |
|---|---|---|
| GET (single) | 200 or 404 | Ok(item) or NotFound() |
| GET (list) | 200 | Ok(items) |
| POST | 201 | CreatedAtAction() |
| PUT | 204 or 404 | NoContent() or NotFound() |
| DELETE | 204 or 404 | NoContent() or NotFound() |
| Validation Error | 400 | BadRequest(ModelState) |
| Auth Required | 401 | Unauthorized() |
| Forbidden | 403 | Forbid() |
| Conflict | 409 | Conflict() |
| Server Error | 500 | Problem() |
Signature Pattern:
public async Task<ActionResult<T>> MethodName(...)
Content Negotiation:
- Use
Ok(data)for automatic format selection - Add
[Produces("application/json", "application/xml")]to document supported formats - Client specifies format via
Acceptheader