Destino Dev

Blog

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