aspnet-core-upload-archivos-iformfile-storage

Uploading files with IFormFile in ASP.NET Core

  • 4 min

IFormFile is the ASP.NET Core interface for receiving files sent in a multipart request.

So far we’ve sent and received text in JSON, but the web also works with files. Sooner or later, a user will want to upload their profile picture or attach a PDF to an invoice.

Handling files is delicate. If you do it wrong, you can exhaust server memory or disk, allow dangerous content, or accept requests much larger than intended.

Let’s receive files, validate them, and save them without dying in the attempt.

Receiving the file (multipart/form-data)

The first thing that changes is the request format. It’s no longer application/json. When you send files, the standard is multipart/form-data.

In your DTO, the file is represented as IFormFile.

public class UploadFileDto
{
    public string FileName { get; set; }
    public string Description { get; set; }

    // 👇 Here comes the binary
    public IFormFile File { get; set; }
}
Copied!

And in the controller, we use [FromForm]:

[HttpPost("upload")]
public async Task<IActionResult> Upload([FromForm] UploadFileDto dto)
{
    if (dto.File == null || dto.File.Length == 0)
        return BadRequest("You haven't sent any file");

    // Saving logic...
    return Ok();
}
Copied!

Security validations

Never trust a file uploaded by a user.

  1. Size: Don’t let them upload a 2 GB video and drain server resources.
  2. Extension and type: Accept an explicit list of formats, not just a list of forbidden extensions.
  3. Name: Don’t trust FileName, as it comes from the client.
// Manual validation (or you could use FluentValidation)
var extension = Path.GetExtension(dto.File.FileName).ToLowerInvariant();
var allowedExtensions = new[] { ".jpg", ".png", ".pdf" };

if (!allowedExtensions.Contains(extension))
    return BadRequest("File type not allowed");

if (dto.File.Length > 10 * 1024 * 1024) // 10 MB
    return BadRequest("The file is too large");
Copied!

The extension helps, but it’s not an absolute guarantee. For sensitive files, it’s advisable to also validate the actual content, pass through an antivirus if applicable, and limit the request size at the server or endpoint level.

Saving to local disk

For small apps or Intranets, saving to a server folder is sufficient.

public async Task<string> SaveToDisk(IFormFile file)
{
    // 1. Path where we'll save (wwwroot/uploads)
    var folder = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads");
    if (!Directory.Exists(folder)) Directory.CreateDirectory(folder);

    // 2. Generate a unique name (to avoid overwriting if two users upload "photo.jpg")
    var extension = Path.GetExtension(file.FileName);
    var uniqueName = $"{Guid.NewGuid():N}{extension}";
    var fullPath = Path.Combine(folder, uniqueName);

    // 3. Copy the stream and close the file when done
    using (var stream = new FileStream(fullPath, FileMode.Create))
    {
        await file.CopyToAsync(stream);
    }

    // Return the relative URL to save in the database
    return $"/uploads/{uniqueName}";
}
Copied!

Preparing cloud storage

Saving to local disk has a serious problem: It doesn’t work well in Docker/Kubernetes. If your API scales and you have 3 containers, and you save the photo in Container A… when the user accesses Container B, the photo won’t exist.

That’s why, in production, we usually save to Azure Blob Storage or AWS S3.

To avoid coupling our code to the local disk, we create an interface in our Application layer:

// IStorageService.cs
public interface IStorageService
{
    Task<string> UploadFile(Stream fileStream, string fileName);
}
Copied!

And we have two implementations in Infrastructure:

  1. LocalStorageService (For development).
  2. AzureBlobStorageService (For production).

Example of local implementation

public class LocalStorageService : IStorageService
{
    private readonly IWebHostEnvironment _env;

    public LocalStorageService(IWebHostEnvironment env)
    {
        _env = env;
    }

    public async Task<string> UploadFile(Stream fileStream, string fileName)
    {
        var folder = Path.Combine(_env.WebRootPath, "uploads");
        Directory.CreateDirectory(folder);

        // fileName must be generated on the server, not come from the client
        var path = Path.Combine(folder, fileName);

        using (var output = new FileStream(path, FileMode.Create))
        {
            await fileStream.CopyToAsync(output);
        }

        // Generate the URL so it's accessible from the browser
        // We need the Request to have the scheme (http/https) and host
        return $"/uploads/{fileName}";
    }
}
Copied!

Serving the files

If you saved the files in wwwroot/uploads, you need to activate the static files middleware in Program.cs (as we saw in the frontend integration article).

app.UseStaticFiles(); // Allows access to http://localhost:5000/uploads/photo.jpg
Copied!

If you use Azure Blob Storage or Amazon S3, you can return a public URL or a temporary signed URL, depending on the file’s privacy. This way, the API doesn’t have to serve every byte directly.