Handling files is notoriously dangerous. A poorly implemented file upload feature can easily lead to Out of Memory exceptions, Path Traversal hacks, or malicious malware execution. Mastering ASP.NET Core's IFormFile, streaming techniques, and strict security validation is essential for enterprise web applications.
ASP.NET Core provides two entirely different paradigms for handling incoming files depending on the file's size.
The entire file is read into the web server's RAM (or a temporary disk buffer) before the controller method even begins executing. Use Case: Small files like user avatars or PDFs (e.g., < 20MB).
The file is processed in small chunks directly from the network stream and piped directly to storage (like AWS S3) without loading the whole file into RAM. Use Case: Large video files, massive datasets (e.g., > 100MB).
Your HTML form must have the enctype="multipart/form-data" attribute, or the file will not be sent.
<form asp-action="UploadAvatar" method="post" enctype="multipart/form-data">
<div class="mb-3">
<label for="file">Select Profile Picture (JPEG/PNG only)</label>
<!-- Accept attribute provides client-side hints, but NEVER trust it -->
<input type="file" name="file" class="form-control" accept=".jpg,.jpeg,.png" />
</div>
<button type="submit" class="btn btn-primary">Upload</button>
</form>
public class ProfileController : Controller
{
private readonly IWebHostEnvironment _env;
// Inject IWebHostEnvironment to safely resolve physical server paths
public ProfileController(IWebHostEnvironment env) => _env = env;
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadAvatar(IFormFile file)
{
// 1. Basic Validation
if (file == null || file.Length == 0)
return BadRequest("File is empty.");
if (file.Length > 5 * 1024 * 1024) // 5MB limit
return BadRequest("File exceeds 5MB limit.");
// 2. Security: Extension Validation
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
var allowedExtensions = new[] { ".jpg", ".jpeg", ".png" };
if (!allowedExtensions.Contains(extension))
return BadRequest("Invalid image format.");
// 3. Security: Generate a RANDOMLY GUID filename.
// NEVER use the client's file.FileName. It can contain "../../../malicious.exe" (Path Traversal)
var trustedFileName = Guid.NewGuid().ToString() + extension;
// 4. Resolve the safe directory (e.g., wwwroot/uploads/avatars)
var uploadPath = Path.Combine(_env.WebRootPath, "uploads", "avatars");
Directory.CreateDirectory(uploadPath); // Ensure it exists
var fullPhysicalPath = Path.Combine(uploadPath, trustedFileName);
// 5. Asynchronously write the buffered IFormFile to the actual physical disk
using (var stream = new FileStream(fullPhysicalPath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
// 6. Save the relative path to your database
var dbPath = $"/uploads/avatars/{trustedFileName}";
// await _db.SaveAvatarPath(userId, dbPath);
return RedirectToAction("Index");
}
}
File downloads use the FileResult base class. There are three primary physical implementations depending on your source data.
public class DownloadController : Controller
{
// 1. PhysicalFile (): When the file exists natively on the web server's hard drive
public IActionResult DownloadPdf()
{
var filepath = Path.Combine(_env.ContentRootPath, "secure_docs", "report.pdf");
return PhysicalFile(filepath, "application/pdf", "FinancialReport2026.pdf");
}
// 2. FileStreamResult via File(): When retrieving from Cloud Storage or a Database Stream
public async Task<IActionResult> DownloadFromAzureBlobObject(string fileId)
{
Stream blobStream = await _azureBlobService.DownloadBlobAsync(fileId);
// The framework automatically disposes the stream when the download finishes
return File(blobStream, "application/octet-stream", "download.zip");
}
// 3. FileContentResult via File(): When generating files in RAM (e.g., creating CSVs on the fly)
public IActionResult DownloadCsv()
{
byte[] csvData = Encoding.UTF8.GetBytes("Id,Name
1,Sandeep
2,John");
return File(csvData, "text/csv", "users.csv");
}
}
virus.exe to virus.jpg. Advanced enterprise apps read the first few bytes (the "magic number") of the file stream to verify the file is genuinely a JPEG before saving it.Q: "Why should you never use `IFormFile` to accept a 2GB video upload in ASP.NET Core?"
Architect Answer: "`IFormFile` works by buffering the entire uploaded file. While ASP.NET Core shifts buffering to disk if the file exceeds a certain limit (default 64KB), the initial ingestion still ties up server resources, and accessing it requires moving massive amounts of IO synchronously. A malicious user could launch a DoS attack by uploading multiple 2GB videos simultaneously, exhausting server disk space or memory. For files over ~50MB, you must abandon Model Binding (`IFormFile`) entirely and use the `MultipartReader` class, reading the raw HTTP Request Body stream directly and piping the bytes outward to cloud storage as they arrive, maintaining a minimal memory footprint."