Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ActionResult Types

Overview Questions

  • IActionResultActionResult<T> khác nhau như thế nào?
  • Khi nào nên dùng IActionResult vs ActionResult<T>?
  • Các helper methods nào có sẵn trong ControllerBase?
  • TypedResults trong Minimal APIs là gì?
  • Làm sao để return custom response format?

IActionResult

Interface cơ bản

// IActionResult là interface base cho tất cả action results
public interface IActionResult
{
    Task ExecuteResultAsync(ActionContext context);
}

Common Action Results

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    // 200 OK
    [HttpGet]
    public IActionResult GetAll()
    {
        return Ok(products);
        // Equivalent: return new OkObjectResult(products);
    }
    
    // 200 OK với object
    [HttpGet("{id}")]
    public IActionResult GetById(int id)
    {
        return Ok(new { id, name = "Product" });
    }
    
    // 201 Created
    [HttpPost]
    public IActionResult Create(Product product)
    {
        var createdProduct = _service.Create(product);
        return CreatedAtAction(
            nameof(GetById), 
            new { id = createdProduct.Id }, 
            createdProduct);
    }
    
    // 204 No Content
    [HttpDelete("{id}")]
    public IActionResult Delete(int id)
    {
        _service.Delete(id);
        return NoContent();
    }
    
    // 400 Bad Request
    [HttpPost("validate")]
    public IActionResult Validate(Product product)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }
        return Ok();
    }
    
    // 404 Not Found
    [HttpGet("{id}")]
    public IActionResult Get(int id)
    {
        var product = _service.GetById(id);
        if (product == null)
        {
            return NotFound();
        }
        return Ok(product);
    }
    
    // 401 Unauthorized
    [HttpGet("secret")]
    public IActionResult GetSecret()
    {
        return Unauthorized();
    }
    
    // 403 Forbidden
    [HttpGet("admin")]
    public IActionResult GetAdmin()
    {
        return Forbid();
    }
    
    // 409 Conflict
    [HttpPost("check-name")]
    public IActionResult CheckName(string name)
    {
        if (_service.NameExists(name))
        {
            return Conflict(new { error = "Name already exists" });
        }
        return Ok();
    }
    
    // 422 Unprocessable Entity
    [HttpPost("process")]
    public IActionResult Process(Order order)
    {
        if (!CanProcess(order))
        {
            return UnprocessableEntity(new { error = "Cannot process order" });
        }
        return Ok();
    }
    
    // 500 Internal Server Error
    [HttpGet("error")]
    public IActionResult TriggerError()
    {
        return StatusCode(500, new { error = "Internal server error" });
    }
}

ActionResult

Generic ActionResult

// ActionResult<T> cho phép return cả IActionResult và T
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    // Return T - tự động wrap thành OkObjectResult
    [HttpGet]
    public ActionResult<List<Product>> GetAll()
    {
        return _service.GetAll();
    }
    
    // Return IActionResult khi cần status code khác
    [HttpGet("{id}")]
    public ActionResult<Product> GetById(int id)
    {
        var product = _service.GetById(id);
        if (product == null)
        {
            return NotFound(); // IActionResult
        }
        return product; // T
    }
    
    // Mixed returns
    [HttpPost]
    public ActionResult<Product> Create(Product product)
    {
        var created = _service.Create(product);
        return CreatedAtAction(
            nameof(GetById), 
            new { id = created.Id }, 
            created);
    }
}

So sánh IActionResult vs ActionResult

AspectIActionResultActionResult
Return typeChỉ IActionResultCả IActionResult và T
Swagger/OpenAPIKhông rõ response typeTự động generate schema
Type safetyKhông
Use caseKhi cần linh hoạtKhi có response type rõ ràng

Content Results

Different Content Types

[HttpGet("json")]
public IActionResult GetJson()
{
    return Json(new { message = "Hello" });
}

[HttpGet("text")]
public IActionResult GetText()
{
    return Content("Hello World", "text/plain");
}

[HttpGet("html")]
public IActionResult GetHtml()
{
    return Content("<h1>Hello</h1>", "text/html");
}

[HttpGet("xml")]
public IActionResult GetXml()
{
    return Content("<message>Hello</message>", "application/xml");
}

File Results

File Downloads

[HttpGet("download")]
public IActionResult DownloadFile()
{
    var bytes = File.ReadAllBytes("path/to/file.pdf");
    return File(bytes, "application/pdf", "document.pdf");
}

[HttpGet("download-stream")]
public IActionResult DownloadStream()
{
    var stream = new FileStream("path/to/file.pdf", FileMode.Open);
    return File(stream, "application/pdf", "document.pdf");
}

[HttpGet("download-virtual")]
public IActionResult DownloadVirtualFile()
{
    return PhysicalFile(
        @"C:\files\document.pdf", 
        "application/pdf", 
        "document.pdf");
}

Redirect Results

Redirects

[HttpGet("redirect")]
public IActionResult RedirectExample()
{
    // 302 Found
    return Redirect("https://example.com");
    
    // 301 Moved Permanently
    return RedirectPermanent("https://example.com");
    
    // Redirect to action
    return RedirectToAction("GetAll");
    
    // Redirect to route
    return RedirectToRoute("default", new { controller = "Home", action = "Index" });
}

TypedResults (Minimal APIs)

.NET 7+ TypedResults

var app = builder.Build();

// TypedResults cung cấp type-safe results
app.MapGet("/api/products/{id}", async (int id, AppDbContext db) =>
{
    var product = await db.Products.FindAsync(id);
    if (product == null)
    {
        return TypedResults.NotFound();
    }
    return TypedResults.Ok(product);
});

app.MapPost("/api/products", async (Product product, AppDbContext db) =>
{
    db.Products.Add(product);
    await db.SaveChangesAsync();
    return TypedResults.Created($"/api/products/{product.Id}", product);
});

app.MapDelete("/api/products/{id}", async (int id, AppDbContext db) =>
{
    var product = await db.Products.FindAsync(id);
    if (product == null)
    {
        return TypedResults.NotFound();
    }
    db.Products.Remove(product);
    await db.SaveChangesAsync();
    return TypedResults.NoContent();
});

TypedResults vs Results

// Results - trả về IActionResult
app.MapGet("/old", () => Results.Ok("Hello"));

// TypedResults - trả về specific type (better for OpenAPI)
app.MapGet("/new", () => TypedResults.Ok("Hello"));

// TypedResults tốt hơn cho OpenAPI generation
app.MapGet("/api/products", () => TypedResults.Ok(new List<Product>()))
    .Produces<List<Product>>(200)
    .ProducesProblem(500);

Custom ActionResult

Custom Result Class

public class PagedResult<T> : IActionResult
{
    private readonly IEnumerable<T> _items;
    private readonly int _total;
    private readonly int _page;
    private readonly int _pageSize;

    public PagedResult(IEnumerable<T> items, int total, int page, int pageSize)
    {
        _items = items;
        _total = total;
        _page = page;
        _pageSize = pageSize;
    }

    public async Task ExecuteResultAsync(ActionContext context)
    {
        var response = context.HttpContext.Response;
        response.ContentType = "application/json";
        
        var result = new
        {
            items = _items,
            total = _total,
            page = _page,
            pageSize = _pageSize,
            totalPages = (int)Math.Ceiling((double)_total / _pageSize)
        };
        
        await JsonSerializer.SerializeAsync(response.Body, result);
    }
}

// Sử dụng
[HttpGet]
public IActionResult GetProducts([FromQuery] int page = 1, [FromQuery] int pageSize = 10)
{
    var (items, total) = _service.GetPaged(page, pageSize);
    return new PagedResult<Product>(items, total, page, pageSize);
}

Best Practices

1. Sử dụng ActionResult cho API rõ ràng

// ✅ Tốt - rõ ràng response type
[HttpGet]
public ActionResult<List<Product>> GetAll() => _service.GetAll();

// ❌ Tránh - không rõ response type
[HttpGet]
public IActionResult GetAll() => Ok(_service.GetAll());

2. Sử dụng helper methods

// ✅ Tốt
return Ok(data);
return NotFound();
return BadRequest(ModelState);

// ❌ Dài dòng
return new ObjectResult(data) { StatusCode = 200 };
return new ObjectResult(null) { StatusCode = 404 };

3. Consistent error responses

// ✅ Sử dụng ProblemDetails
return Problem(
    title: "Not Found",
    detail: $"Product with id {id} not found",
    statusCode: 404);

// ❌ Inconsistent
return NotFound(new { error = "Not found", message = "Product not found" });