Upload de arquivos com ASP.NET Core Minimal APIs: streaming e progresso
Por John Pires · 10 de março de 2026

1) Introducao ao problema
O que significa fazer upload de arquivos
Fazer upload de arquivos é a ação de receber dados binários do cliente (navegador, app móvel, etc.) para o servidor. Para iniciantes, imagine uma esteira: o cliente coloca caixas (os arquivos) e o servidor recebe e coloca essas caixas em prateleiras (disco, blob storage, banco). O desafio técnico vem quando as caixas são grandes, a conexão é instável, ou você precisa mostrar progresso, evitar o estouro de memória e manter a aplicação responsiva.
Por que streaming e progresso importam
Stream significa processar dados em pedaços (chunks) em vez de carregar tudo na memória. Para arquivos grandes, ler tudo na memória causa OutOfMemoryException. Progresso é necessário para boa UX: mostrar quanto já foi enviado, estimar tempo, e permitir reações como cancelar upload. Em aplicações reais, combinar streaming com um canal para relatar progresso é prática comum.
2) Pre-requisitos e setup
Ferramentas mínimas
É preciso ter .NET 7+ (ou 6), o SDK instalado, um editor (VS Code, Visual Studio), e conhecimento básico de C# e HTTP. Vamos usar Minimal APIs no Program.cs para manter o exemplo simples. Também usamos armazenamento local (filesystem) para facilitar a compreensão.
Estrutura do projeto
Crie um projeto do tipo web minimal API: dotnet new web -n UploadDemo. A estrutura mínima terá apenas Program.cs. Vamos configurar endpoints para: iniciar upload (receber arquivo), salvar via streaming, reportar progresso (polling) e cuidar de erros comuns.
3) Tutorial guiado passo a passo
Visão geral do fluxo
Fluxo que vamos implementar:
- Cliente faz POST /upload?uploadId={id} com multipart/form-data contendo o arquivo.
- Servidor lê o corpo em chunks e grava no disco sem alocar tudo em memória.
- Servidor atualiza um registro de progresso em memória associado ao uploadId.
- Cliente faz GET /upload/progress/{uploadId} periodicamente para obter porcentagem.
Essa abordagem é simples para iniciantes, evita complexidade de WebSockets e é eficaz para a maioria dos casos.
Configuração inicial do Program.cs
Primeiro, crie um dicionário thread-safe para armazenar progresso. Em Program.cs, registre endpoints e use configuração mínima para uploads grandes (opcional).
O trecho abaixo cria o dicionário e configura rotas básicas. Ele prepara o ambiente para os exemplos seguintes.
// Contexto: configuração mínima em Program.cs — cria o armazenamento de progresso e configura o pipeline
using System.Collections.Concurrent;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Hosting;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Armazenamento simples em memória para progresso (apenas para demonstração)
var uploadProgress = new ConcurrentDictionary<string, long>();
app.MapGet("/health", () => Results.Ok("ok"));
app.Run();
Depois de adicionar isso e rodar dotnet run, você tem uma API mínima. Abaixo vamos expandir com os endpoints reais de upload e progresso.
Endpoint: upload com streaming e reporting
Agora implementamos o endpoint POST /upload. Ele lê o IFormFile se o conteúdo vier como multipart/form-data; para grandes uploads também vamos suportar leitura direta de Request.Body. O trecho faz leitura em blocos e atualiza o dicionário uploadProgress.
// Contexto: endpoint que recebe upload via multipart/form-data e salva com streaming atualizando progresso
app.MapPost("/upload", async (HttpRequest request) =>
{
// Identificador enviado como query: ?uploadId=abc123
var uploadId = request.Query["uploadId"].ToString();
if (string.IsNullOrEmpty(uploadId))
return Results.BadRequest("uploadId query parameter is required");
// Se for multipart, pegamos o arquivo; caso contrário, lemos Request.Body diretamente
if (!request.HasFormContentType)
return Results.BadRequest("Content-Type must be multipart/form-data");
var form = await request.ReadFormAsync();
var file = form.Files.GetFile("file");
if (file == null)
return Results.BadRequest("form field 'file' is required");
var savePath = Path.Combine("uploads", Path.GetFileName(file.FileName));
Directory.CreateDirectory("uploads");
// Inicia o progresso com zero
uploadProgress[uploadId] = 0;
// Faz streaming: lê do arquivo em blocos e grava no disco
var buffer = new byte[81920]; // 80 KB — padrão eficiente
long totalRead = 0;
using (var source = file.OpenReadStream())
using (var target = System.IO.File.Create(savePath))
{
int read;
while ((read = await source.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
await target.WriteAsync(buffer.AsMemory(0, read));
totalRead += read;
uploadProgress[uploadId] = totalRead; // atualiza bytes recebidos
}
}
// Opcional: remover progresso
uploadProgress.TryRemove(uploadId, out _);
return Results.Ok(new { saved = savePath });
});
O que acontece: o arquivo é lido em blocos de 80KB e gravado no disco, mantendo apenas o buffer na memória. O dicionário uploadProgress registra os bytes recebidos para que um endpoint de progresso possa consultar.
Endpoint: consultar progresso (polling)
Cliente faz polling para saber quantos bytes já foram recebidos. O endpoint converte bytes em porcentagem se o cliente informar o tamanho total esperado.
// Contexto: endpoint que retorna progresso atual do upload em bytes e opcionalmente em porcentagem
app.MapGet("/upload/progress/{uploadId}", (string uploadId, HttpRequest request) =>
{
if (!uploadProgress.TryGetValue(uploadId, out var bytesReceived))
return Results.NotFound(new { error = "uploadId not found or already finished" });
// Se o cliente passou o tamanho total, calcula porcentagem
long.TryParse(request.Query["totalBytes"], out var totalBytes);
var percent = totalBytes > 0 ? (int)((bytesReceived * 100) / totalBytes) : (int?)null;
return Results.Ok(new { uploadId, bytesReceived, totalBytes = totalBytes == 0 ? (long?)null : totalBytes, percent });
});
app.Run();
O que acontece: o servidor retorna os bytes já recebidos. O cliente pode passar totalBytes para receber porcentagem. Em produção, você teria valido concorrência e segurança (autenticação/authorization) sobre uploadId.
Erros comuns e armadilhas
Upload parece simples, mas há pegadinhas frequentes. Abaixo duas categorias de problemas e como evitá-las.
Limites de tamanho e configurações
Por padrão, o ASP.NET Core pode limitar o tamanho de requisições. Se seu upload falhar em arquivos maiores, verifique FormOptions e Kestrel limits. Em Program.cs você pode aumentar limites via builder.WebHost.ConfigureKestrel(...) ou ajustar MultipartBodyLengthLimit.
OutOfMemory e leitura incorreta
Um erro comum é usar IFormFile.CopyToAsync sem atenção ao tamanho: embora CopyToAsync faça streaming, muitos tutoriais mostram leitura inteira em memória. Sempre prefira ReadAsync em blocos ou CopyToAsync para FileStream sem materializar byte[] inteiro. Monitore o uso de memória e evite buffers gigantes.
Regra prática: leia em blocos (32-256KB) e não mantenha o arquivo inteiro na memória.
4) Resultado final funcionando
Passos para testar localmente
Resumo prático para testar: 1) dotnet run no projeto, 2) usar curl ou Postman para enviar POST /upload?uploadId=meu-id com multipart/form-data (campo file), 3) consultar GET /upload/progress/meu-id?totalBytes=123456. Abaixo um exemplo rápido de curl para upload (não é C# mas útil para test):
Exemplo de uso: com o servidor rodando em http://localhost:5000, execute um POST com curl para enviar o arquivo e um GET para ver progresso.
O código do servidor que demonstramos grava o arquivo em uploads/ e atualiza uploadProgress com os bytes gravados. Enquanto o upload está ocorrendo, requisições ao endpoint /upload/progress/{uploadId} retornam progresso atualizado.
Observações sobre confiabilidade
Para produção, considere:
- Persistir progresso em cache distribuído (Redis) se sua aplicação tiver múltiplas instâncias.
- Autenticar e autorizar o uploadId — evitar que qualquer um consulte progresso de outros.
- Implementar retry/tempo limite e limpeza de entradas antigas em uploadProgress.
5) Desafio para o leitor praticar
Desafio 1: suportar resumable upload (tentar chunked resumable)
Implemente no servidor suporte a uploads em múltiplos chunks com cabeçalho indicando offset e total. O cliente envia partes numeradas e o servidor reconstroi incrementando o offset. Este exercício ensina a lidar com concorrência e verificação de integridade.
Desafio 2: trocar polling por WebSocket ou SignalR
Substitua o polling por uma notificação em tempo real com WebSocket ou SignalR. O servidor envia atualizações de progresso ao cliente via canal persistente. Isso evita chamadas repetidas e reduz latência de UX.
6) Conclusao pratica
Resumo dos pontos principais
Você aprendeu a capturar uploads em Minimal APIs usando leitura em blocos (streaming), a armazenar progresso em memória e a expor um endpoint de progress para polling. Estratégias como streaming minimizam uso de memória e aumentam confiabilidade em arquivos grandes. Polling é simples e funciona; para UX superior, use SignalR/WebSockets.
Próximos passos recomendados
Implemente um protótipo com persistência (Redis), adicione autenticação (JWT), e crie um cliente que faça upload em chunks com retry e exiba a barra de progresso. Teste com arquivos grandes para validar comportamento do servidor sob carga.
Se algo falhar no seu teste: verifique logs, limites do servidor (Kestrel), e os headers de request. Corrija os buffers e monitore o uso de memória.
Boa prática final: sempre limpe recursos temporários e tenha um mecanismo para remover entradas obsoletas em uploadProgress (ex.: expirations).
Assuntos: C#, aspnet-core, minimal-apis, file-upload, streaming
Leia também
Autenticação JWT em Minimal APIs: refresh tokens seguros com HttpOnly
Guia prático para implementar refresh tokens seguros com cookies HttpOnly em Minimal APIs ASP.NET Core: conceitos, passos comentados, exemplos em C# e…
Consultas dinâmicas seguras com Expression Trees e Specification no EF Core
Como construir consultas dinâmicas seguras no EF Core usando Expression Trees e Specification: padrões, código pronto e soluções para problemas de tradução e…
EF Core: Upsert, ExecuteUpdate/ExecuteDelete e estratégias de operações em lote
Guia prático para aplicar upserts e operações em lote com EF Core: quando usar ExecuteUpdate/ExecuteDelete, MERGE/ON CONFLICT, bibliotecas de bulk e patterns…
Minimal APIs + SSE: enviar progresso de jobs para clientes Blazor passo a passo
Guia passo a passo para usar Minimal APIs + SSE e enviar progresso de jobs para clientes Blazor. Do setup ao código funcional: servidor com endpoint SSE…
JWT em Minimal APIs (ASP.NET Core): access e refresh tokens do zero
Aprenda do zero como implementar access e refresh tokens com JWT em Minimal APIs do ASP.NET Core: código real, armazenamento de refresh token, endpoints…
Multitenancy em ASP.NET Core com EF Core: discriminator, schema e database
Comparação prática entre discriminator, schema e database para multitenancy em ASP.NET Core com EF Core. Implementação de TenantProvider, DbContext dinâmico…