In this guide, we will create a reusable Extension class to handle file validation (Type & Size) and the saving process cleanly.
Step 1: Define the Size Enums
First, to make our size validation readable, let's define a simple Enum. This avoids "magic numbers" in our code
namespace CoreApp.Utilities.Enums
{
public enum FileSize
{
KB,
MB,
GB
}
}
Step 2: Setting up the Validator Extension
We create a static class Validator within the Utilities.Extensions namespace. This class will extend IFormFile, allowing us to call methods directly on the file object (e.g., file.ValidateSize(...)).
- File Type Validation
We need to ensure the user is uploading the correct format (e.g., preventing a .exe file when we expect an image).
using CoreApp.Utilities.Enums;
namespace CoreApp.Utilities.Extensions
{
public static class Validator
{
// Extension method to check file MIME type (e.g., "image/")
public static bool ValidateType(this IFormFile formFile, string type)
{
// Checks if the ContentType contains the string (e.g. "image")
return formFile.ContentType.Contains(type);
}
- File Size Validation
Next, we implement the logic to check if a file exceeds a specific limit. Using our FileSize enum makes the call site very readable.
// Returns TRUE if the file is LARGER than the limit
public static bool ValidateSize(this IFormFile formFile, FileSize fileSize, int size)
{
switch (fileSize)
{
case FileSize.KB:
return formFile.Length > size * 1024;
case FileSize.MB:
return formFile.Length > size * 1024 * 1024;
case FileSize.GB:
return formFile.Length > size * 1024 * 1024 * 1024;
}
return false;
}
Step 3: The Save Logic (Async)
This is the most critical part. We need to:
Generate a unique filename (using Guid) to prevent overwriting existing files.
Combine paths safely using Path.Combine (works on Windows/Linux).
Save the file asynchronously to avoid blocking the thread.
// Handles the unique naming and saving process
public async static Task<string> CreateFileAsync(this IFormFile formFile, params string[] roots)
{
// 1. Generate unique name (Guid + Original Name)
string fileName = string.Concat(Guid.NewGuid().ToString(), formFile.FileName);
string path = string.Empty;
// 2. Combine path parts dynamically
for (int i = 0; i < roots.Length; i++)
{
path = Path.Combine(path, roots[i]);
}
// Append the filename to the final path
path = Path.Combine(path, fileName);
// 3. Create the stream and copy the file content
using (FileStream fileStream = new(path, FileMode.Create))
{
await formFile.CopyToAsync(fileStream);
}
// Return the name to save in the Database
return fileName;
}
}
}
Why did we use Async/Await?
You might notice we used CopyToAsync instead of CopyTo. Here is why asynchronous programming is critical for File Uploads.
- Non-Blocking I/O (Scalability)
Saving a file is an I/O bound operation. If we use synchronous code, the thread waits until the file is fully saved. With async, the thread is freed up to handle other user requests while the disk writes the file.
// ❌ BAD: Blocks the thread (Server freezes for other users)
formFile.CopyTo(stream);
// ✅ GOOD: Thread is free while file saves
await formFile.CopyToAsync(stream);
- Parallel Execution (Performance) If a user uploads multiple product images (e.g., a Gallery), we don't need to save them one by one. using Task.WhenAll, we can save 5 images simultaneously!
public async Task<List<string>> UploadMultipleAsync(List<IFormFile> files)
{
var tasks = new List<Task<string>>();
foreach (var file in files)
{
// Starts the task but doesn't wait yet
tasks.Add(file.CreateFileAsync("wwwroot", "uploads"));
}
// Runs all uploads at the exact same time
string[] results = await Task.WhenAll(tasks);
return results.ToList();
}
public async Task<List<string>> UploadMultipleAsync(List<IFormFile> files)
{
var tasks = new List<Task<string>>();
foreach (var file in files)
{
// Starts the task but doesn't wait yet
tasks.Add(file.CreateFileAsync("wwwroot", "uploads"));
}
// Runs all uploads at the exact same time
string[] results = await Task.WhenAll(tasks);
return results.ToList();
}
How to Use It in a Controller?
Now, look at how clean our Controller becomes. No heavy logic, just simple checks!
[HttpPost]
public async Task<IActionResult> Create(CreateProductVM model)
{
// 1. Check if it's an image
if (!model.Photo.ValidateType("image/"))
{
ModelState.AddModelError("Photo", "Please upload a valid image file.");
return View(model);
}
// 2. Check if it's smaller than 2MB
if (model.Photo.ValidateSize(FileSize.MB, 2))
{
ModelState.AddModelError("Photo", "File size must be less than 2MB.");
return View(model);
}
// 3. Save the file and get the name
// Saves to: wwwroot/assets/images/products/
string fileName = await model.Photo.CreateFileAsync(_env.WebRootPath, "assets", "images", "products");
// ... Save to DB logic here ...
return RedirectToAction(nameof(Index));
}
Conclusion
By using Extension Methods, we moved the infrastructure logic out of the Controller. This makes our code testable, reusable, and strictly follows the Single Responsibility Principle.

Top comments (0)