IActionResult và ActionResult<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 là interface base cho tất cả action results
public interface IActionResult
{
Task ExecuteResultAsync(ActionContext context);
}
[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<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);
}
}
Aspect IActionResult ActionResult
Return type Chỉ IActionResult Cả IActionResult và T
Swagger/OpenAPI Không rõ response type Tự động generate schema
Type safety Không Có
Use case Khi cần linh hoạt Khi có response type rõ ràng
[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");
}
[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");
}
[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" });
}
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();
});
// 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);
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);
}
// ✅ 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());
// ✅ 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 };
// ✅ 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" });