Khi xây dựng backend bằng ASP.NET Core Web API, việc xử lý yêu cầu tải lên tệp tin (file upload) thường gặp một số trở ngại liên quan đến ràng buộc mô hình (model binding) và định tuyến. Bài viết này sẽ phân tích các vấn đề phổ biến khi sử dụng multipart/form-data và cung cấp giải pháp tối ưu để truyền tải cả tệp tin lẫn dữ liệu biểu mẫu đi kèm.
Khắc phục lỗi định tuyến với multipart/form-data
Theo mặc định, các template của Web API thường tạo ra các action method sử dụng thuộc tính [FromBody] để đọc dữ liệu từ body của request dưới dạng JSON:
[HttpPost]
public IActionResult CreateItem([FromBody] string payload)
{
// Xử lý dữ liệu JSON
return Ok();
}
Tuy nhiên, khi client gửi yêu cầu tải lên tệp tin, Content-Type của request phải là multipart/form-data. Nếu bạn cố gắng truy cập Request.Form.Files trực tiếp trong một action được cấu hình cho JSON, hệ thống sẽ không thể định tuyến hoặc ném ra lỗi. Để khắc phục, bạn cần khai báo rõ ràng rằng Controller hoặc Action đó chấp nhận multipart/form-data thông qua thuộc tính [Consumes]:
[ApiController]
[Route("api/[controller]")]
[Consumes("multipart/form-data")]
public class DocumentsController : ControllerBase
{
// Các action method
}
Tiếp nhận Tệp tin trong Action Method
Để nhận tệp tin, bạn có thể sử dụng interface IFormCollection (tương đương với Request.Form) hoặc IFormFile. Trong nhiều trường hợp, việc sử dụng trực tiếp IFormFile bị trả về null thường do tên của input file ở frontend không khớp với tên tham số ở backend.
Dưới đây là cách tiếp cận an toàn và linh hoạt nhất bằng cách sử dụng IFormCollection để duyệt qua tất cả các tệp tin được gửi lên:
[HttpPost("upload-multiple")]
public async Task<IActionResult> UploadMultipleFiles(IFormCollection formData)
{
var uploadedFiles = formData.Files;
foreach (var document in uploadedFiles)
{
var fileName = document.FileName;
var fileSize = document.Length;
using (var memoryStream = new MemoryStream())
{
await document.CopyToAsync(memoryStream);
// Xử lý stream tại đây (lưu vào DB, upload lên cloud, v.v.)
}
}
return Ok(new { Message = "Tải lên thành công", FileCount = uploadedFiles.Count });
}
Lưu ý: IFormFile không cung cấp các phương thức đọc ghi trực tiếp như System.IO.File. Thay vào đó, nó cung cấp phương thức OpenReadStream() hoặc CopyToAsync() để làm việc với luồng dữ liệu (stream).
Tải lên đồng thời Tệp tin và Dữ liệu Metadata
Trong thực tế, chúng ta hiếm khi chỉ tải lên mỗi tệp tin mà thường cần gửi kèm các thông tin mô tả (metadata) như loại tài liệu, người tạo, hoặc ghi chú. Một sai lầm phổ biến là cố gắng kết hợp [FromBody] cho dữ liệu chuỗi và IFormCollection cho tệp tin:
// CÁCH LÀM SAI - Sẽ gây lỗi hoặc trả về null
[HttpPost("upload-with-metadata-bad")]
public IActionResult UploadBadPractice([FromBody] string docType, IFormCollection files)
{
// docType sẽ bị null hoặc request báo lỗi
return Ok();
}
Lý do cho sự thất bại này là luồng (stream) của request body không được đệm (non-buffered) và chỉ có thể được đọc một lần. Khi framework cố gắng đọc body cho [FromBody], nó sẽ tiêu thụ luồng đó, khiến phần đọc form data bị trống.
Giải pháp chuẩn: Hãy loại bỏ [FromBody] và sử dụng [FromForm]. Cách tốt nhất là đóng gói tất cả dữ liệu (bao gồm cả tệp tin và metadata) vào một Data Transfer Object (DTO) hoặc ViewModel.
public class DocumentUploadDto
{
public string DocumentType { get; set; }
public string Author { get; set; }
public IFormFile FileContent { get; set; } // Hoặc List<IFormFile> nếu cần nhiều file
}
[HttpPost("upload-with-metadata")]
public async Task<IActionResult> UploadWithMetadata([FromForm] DocumentUploadDto requestDto)
{
if (requestDto.FileContent == null || requestDto.FileContent.Length == 0)
{
return BadRequest("Tệp tin không được để trống.");
}
var fileType = requestDto.DocumentType;
var authorName = requestDto.Author;
var safeFileName = $"{Guid.NewGuid()}_{Path.GetFileName(requestDto.FileContent.FileName)}";
var targetPath = Path.Combine(Directory.GetCurrentDirectory(), "Uploads", safeFileName);
using (var fileStream = new FileStream(targetPath, FileMode.Create))
{
await requestDto.FileContent.CopyToAsync(fileStream);
}
return Ok(new
{
Message = "Lưu tài liệu thành công",
FilePath = targetPath,
Metadata = new { fileType, authorName }
});
}
Bằng cách sử dụng [FromForm] kết hợp với một lớp DTO, ASP.NET Core sẽ tự động phân tích cú pháp của multipart/form-data, ánh xạ chính xác các trường văn bản vào thuộc tính chuỗi và ánh xạ các phần tệp tin vào thuộc tính IFormFile. Điều này giúp code sạch hơn, dễ bảo trì và tránh hoàn toàn xung đột khi đọc request stream.