- Làm sao để upload file trong ASP.NET Core?
IFormFile là gì và sử dụng như thế nào?
- Streaming upload khác buffering như thế nào?
- Làm sao để handle large file uploads?
- Download file được implement ra sao?
[HttpPost("upload")]
public async Task<IActionResult> UploadFile(IFormFile file)
{
if (file == null || file.Length == 0)
{
return BadRequest("No file uploaded");
}
// Validate file size (max 10MB)
if (file.Length > 10 * 1024 * 1024)
{
return BadRequest("File too large");
}
// Validate file type
var allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif" };
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
if (!allowedExtensions.Contains(extension))
{
return BadRequest("Invalid file type");
}
// Save file
var fileName = $"{Guid.NewGuid()}{extension}";
var filePath = Path.Combine("uploads", fileName);
using var stream = new FileStream(filePath, FileMode.Create);
await file.CopyToAsync(stream);
return Ok(new { fileName, filePath });
}
[HttpPost("upload-multiple")]
public async Task<IActionResult> UploadFiles(List<IFormFile> files)
{
if (files == null || files.Count == 0)
{
return BadRequest("No files uploaded");
}
var uploadedFiles = new List<string>();
foreach (var file in files)
{
var fileName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
var filePath = Path.Combine("uploads", fileName);
using var stream = new FileStream(filePath, FileMode.Create);
await file.CopyToAsync(stream);
uploadedFiles.Add(fileName);
}
return Ok(uploadedFiles);
}
public class ProductForm
{
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public IFormFile? Image { get; set; }
public List<IFormFile>? Documents { get; set; }
}
[HttpPost("products")]
public async Task<IActionResult> CreateProduct([FromForm] ProductForm form)
{
// Process form data
var product = new Product
{
Name = form.Name,
Price = form.Price
};
// Process image
if (form.Image != null && form.Image.Length > 0)
{
var fileName = $"{Guid.NewGuid()}{Path.GetExtension(form.Image.FileName)}";
var filePath = Path.Combine("uploads", "images", fileName);
using var stream = new FileStream(filePath, FileMode.Create);
await form.Image.CopyToAsync(stream);
product.ImageUrl = $"/uploads/images/{fileName}";
}
// Process documents
if (form.Documents != null)
{
foreach (var doc in form.Documents)
{
// Save documents
}
}
return Ok(product);
}
┌─────────────────────────────────────────────────────────────────┐
│ UPLOAD METHODS │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Buffering (Default): │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Client │───▶│ Memory │───▶│ Disk │ │
│ │ Upload │ │ (Form) │ │ (Save) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ - Toàn bộ file load vào memory trước │
│ - Phù hợp cho files nhỏ (< 64KB) │
│ │
│ Streaming: │
│ ┌──────────┐ ┌──────────┐ │
│ │ Client │───▶│ Disk │ │
│ │ Upload │ │ (Save) │ │
│ └──────────┘ └──────────┘ │
│ - Stream trực tiếp vào disk │
│ - Phù hợp cho files lớn │
│ │
└─────────────────────────────────────────────────────────────────┘
[HttpPost("upload-stream")]
[RequestSizeLimit(100 * 1024 * 1024)] // 100MB
public async Task<IActionResult> UploadStream()
{
var fileName = Request.Headers["X-File-Name"].FirstOrDefault();
if (string.IsNullOrEmpty(fileName))
{
return BadRequest("Missing file name header");
}
var filePath = Path.Combine("uploads", fileName);
// Stream trực tiếp vào file
using var stream = new FileStream(filePath, FileMode.Create);
await Request.Body.CopyToAsync(stream);
return Ok(new { fileName, size = stream.Length });
}
[HttpPost("upload-multipart-stream")]
[DisableFormValueModelBinding] // Custom attribute để disable automatic binding
public async Task<IActionResult> UploadMultipart()
{
if (!Request.HasFormContentType)
{
return BadRequest("Invalid content type");
}
var form = await Request.ReadFormAsync();
var file = form.Files["file"];
if (file == null || file.Length == 0)
{
return BadRequest("No file");
}
var fileName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
var filePath = Path.Combine("uploads", fileName);
using var stream = new FileStream(filePath, FileMode.Create);
await file.CopyToAsync(stream);
return Ok(new { fileName });
}
// Attribute để disable form binding
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
var factories = context.ValueProviderFactories;
factories.RemoveType<FormValueProviderFactory>();
factories.RemoveType<FormFileValueProviderFactory>();
factories.RemoveType<JQueryFormValueProviderFactory>();
}
public void OnResourceExecuted(ResourceExecutedContext context) { }
}
[HttpGet("download/{fileName}")]
public IActionResult Download(string fileName)
{
var filePath = Path.Combine("uploads", fileName);
if (!System.IO.File.Exists(filePath))
{
return NotFound();
}
var contentType = GetContentType(fileName);
return PhysicalFile(filePath, contentType, fileName);
}
private string GetContentType(string fileName)
{
var extension = Path.GetExtension(fileName).ToLowerInvariant();
return extension switch
{
".jpg" or ".jpeg" => "image/jpeg",
".png" => "image/png",
".pdf" => "application/pdf",
".txt" => "text/plain",
_ => "application/octet-stream"
};
}
[HttpGet("download-stream/{fileName}")]
public IActionResult DownloadStream(string fileName)
{
var filePath = Path.Combine("uploads", fileName);
if (!System.IO.File.Exists(filePath))
{
return NotFound();
}
var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
return File(stream, "application/octet-stream", fileName);
}
[HttpGet("download-bytes")]
public IActionResult DownloadBytes()
{
var bytes = GeneratePdf(); // Generate file in memory
return File(bytes, "application/pdf", "report.pdf");
}
// Program.cs
builder.Services.Configure<FormOptions>(options =>
{
options.MultipartBodyLengthLimit = 100 * 1024 * 1024; // 100MB
options.ValueLengthLimit = int.MaxValue;
options.MultipartHeadersLengthLimit = int.MaxValue;
});
// Hoặc trong appsettings.json
{
"Kestrel": {
"Limits": {
"MaxRequestBodySize": 104857600 // 100MB
}
}
}
// Per-action limit
[HttpPost("upload")]
[RequestSizeLimit(50 * 1024 * 1024)] // 50MB
public async Task<IActionResult> Upload(IFormFile file)
{
// ...
}
// No limit
[HttpPost("upload-large")]
[DisableRequestSizeLimit]
public async Task<IActionResult> UploadLarge(IFormFile file)
{
// ...
}
public class FileUploadValidator
{
private static readonly Dictionary<string, string[]> AllowedExtensions = new()
{
["image"] = new[] { ".jpg", ".jpeg", ".png", ".gif", ".webp" },
["document"] = new[] { ".pdf", ".doc", ".docx" },
["video"] = new[] { ".mp4", ".avi", ".mov" }
};
public static (bool IsValid, string? Error) Validate(IFormFile file, string category)
{
if (file == null || file.Length == 0)
return (false, "No file uploaded");
if (file.Length > 100 * 1024 * 1024)
return (false, "File too large (max 100MB)");
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
if (!AllowedExtensions.TryGetValue(category, out var allowed) ||
!allowed.Contains(extension))
{
return (false, $"Invalid file type. Allowed: {string.Join(", ", allowed)}");
}
return (true, null);
}
}
// ✅ Good - generate safe file name
var safeFileName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
// ❌ Bad - use original file name (security risk)
var fileName = file.FileName; // Could contain path traversal
// Integrate with antivirus scanning
public async Task<bool> ScanForMalwareAsync(Stream fileStream)
{
// Use antivirus API or external service
// Return true if clean
return true;
}