aspnet-core-upload-archivos-iformfile-storage

Subir archivos con IFormFile en ASP.NET Core

  • 4 min

IFormFile es la interfaz de ASP.NET Core para recibir archivos enviados en una petición multipart.

Hasta ahora hemos enviado y recibido texto en JSON, pero la web también trabaja con archivos. Tarde o temprano, un usuario querrá subir su foto de perfil o adjuntar un PDF a una factura.

Manejar archivos es delicado. Si lo haces mal, puedes agotar la memoria o el disco del servidor, permitir contenido peligroso o aceptar peticiones mucho mayores de lo previsto.

Vamos a recibir archivos, validarlos y guardarlos sin morir en el intento.

Recibir el archivo (multipart/form-data)

Lo primero que cambia es el formato de la petición. Ya no es application/json. Cuando envías archivos, el estándar es multipart/form-data.

En tu DTO, el archivo se representa como IFormFile.

public class SubirArchivoDto
{
    public string NombreArchivo { get; set; }
    public string Descripcion { get; set; }

    // 👇 Aquí viene el binario
    public IFormFile Archivo { get; set; }
}
Copied!

Y en el controlador, usamos [FromForm]:

[HttpPost("subir")]
public async Task<IActionResult> Subir([FromForm] SubirArchivoDto dto)
{
    if (dto.Archivo == null || dto.Archivo.Length == 0)
        return BadRequest("No has enviado ningún archivo");

    // Lógica de guardado...
    return Ok();
}
Copied!

Validaciones de seguridad

Nunca confíes en un archivo subido por un usuario.

  1. Tamaño: No dejes que te suban un vídeo de 2 GB y agoten los recursos del servidor.
  2. Extensión y tipo: Acepta una lista explícita de formatos, no solo una lista de extensiones prohibidas.
  3. Nombre: No confíes en FileName, porque viene del cliente.
// Validación manual (o podrías usar FluentValidation)
var extension = Path.GetExtension(dto.Archivo.FileName).ToLowerInvariant();
var extensionesPermitidas = new[] { ".jpg", ".png", ".pdf" };

if (!extensionesPermitidas.Contains(extension))
    return BadRequest("Tipo de archivo no permitido");

if (dto.Archivo.Length > 10 * 1024 * 1024) // 10 MB
    return BadRequest("El archivo es demasiado grande");
Copied!

La extensión ayuda, pero no es una garantía absoluta. Para ficheros sensibles, conviene validar también el contenido real, pasar antivirus si aplica y limitar el tamaño de petición en servidor o endpoint.

Guardar en disco local

Para apps pequeñas o Intranets, guardar en una carpeta del servidor es suficiente.

public async Task<string> GuardarEnDisco(IFormFile archivo)
{
    // 1. Ruta donde guardaremos (wwwroot/uploads)
    var carpeta = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads");
    if (!Directory.Exists(carpeta)) Directory.CreateDirectory(carpeta);

    // 2. Generar nombre único (para no sobrescribir si dos usuarios suben "foto.jpg")
    var extension = Path.GetExtension(archivo.FileName);
    var nombreUnico = $"{Guid.NewGuid():N}{extension}";
    var rutaCompleta = Path.Combine(carpeta, nombreUnico);

    // 3. Copiar el stream y cerrar el archivo al terminar
    using (var stream = new FileStream(rutaCompleta, FileMode.Create))
    {
        await archivo.CopyToAsync(stream);
    }

    // Devolvemos la URL relativa para guardarla en BBDD
    return $"/uploads/{nombreUnico}";
}
Copied!

Preparar el almacenamiento en la nube

Guardar en disco local tiene un problema grave: No funciona bien en Docker/Kubernetes. Si tu API escala y tienes 3 contenedores, y guardas la foto en el Contenedor A… cuando el usuario entre al Contenedor B, la foto no existirá.

Por eso, en producción, solemos guardar en Azure Blob Storage o AWS S3.

Para no acoplar nuestro código al disco local, creamos una interfaz en nuestra capa Application:

// IStorageService.cs
public interface IStorageService
{
    Task<string> SubirArchivo(Stream archivoStream, string nombreArchivo);
}
Copied!

Y tenemos dos implementaciones en Infrastructure:

  1. LocalStorageService (Para desarrollo).
  2. AzureBlobStorageService (Para producción).

Ejemplo de implementación local

public class LocalStorageService : IStorageService
{
    private readonly IWebHostEnvironment _env;

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

    public async Task<string> SubirArchivo(Stream archivoStream, string nombreArchivo)
    {
        var carpeta = Path.Combine(_env.WebRootPath, "uploads");
        Directory.CreateDirectory(carpeta);

        // nombreArchivo debe haberse generado en el servidor, no venir del cliente
        var ruta = Path.Combine(carpeta, nombreArchivo);

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

        // Generamos la URL para que sea accesible desde el navegador
        // Necesitamos que el Request tenga el esquema (http/https) y el host
        return $"/uploads/{nombreArchivo}";
    }
}
Copied!

Servir los archivos

Si has guardado los archivos en wwwroot/uploads, necesitas activar el middleware de archivos estáticos en Program.cs (como vimos en el artículo de integración frontend).

app.UseStaticFiles(); // Permite acceder a http://localhost:5000/uploads/foto.jpg
Copied!

Si usas Azure Blob Storage o Amazon S3, puedes devolver una URL pública o una URL firmada temporal, según la privacidad del archivo. Así la API no tiene que servir cada byte directamente.