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

File Upload & Download

Overview Questions

  • 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?

File Upload với IFormFile

Single File Upload

[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 });
}

Multiple File Upload

[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);
}

Upload với Form Data

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);
}

Streaming Upload (Large Files)

Streaming vs Buffering

┌─────────────────────────────────────────────────────────────────┐
│                    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                                       │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Streaming Implementation

[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 });
}

Multipart Streaming

[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) { }
}

File Download

Download từ File

[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"
    };
}

Download từ 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);
}

Download từ Byte Array

[HttpGet("download-bytes")]
public IActionResult DownloadBytes()
{
    var bytes = GeneratePdf(); // Generate file in memory
    return File(bytes, "application/pdf", "report.pdf");
}

Configuration

File Size Limits

// 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
    }
  }
}

Request Size Limit Attribute

// 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)
{
    // ...
}

Best Practices

1. Validate File Uploads

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);
    }
}

2. Use Safe File Names

// ✅ 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

3. Scan for Malware

// Integrate with antivirus scanning
public async Task<bool> ScanForMalwareAsync(Stream fileStream)
{
    // Use antivirus API or external service
    // Return true if clean
    return true;
}