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; }
}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();
}Validaciones de seguridad
Nunca confíes en un archivo subido por un usuario.
- Tamaño: No dejes que te suban un vídeo de 2 GB y agoten los recursos del servidor.
- Extensión y tipo: Acepta una lista explícita de formatos, no solo una lista de extensiones prohibidas.
- 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");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}";
}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);
}Y tenemos dos implementaciones en Infrastructure:
LocalStorageService(Para desarrollo).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}";
}
}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.jpgSi 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.