Skip to main content

Streams | CSharp

  • Streams | CSharp

O que é Stream

Stream é uma abstração de fluxo contínuo de dados no .NET. Ele representa um canal pelo qual bytes são lidos ou escritos de forma sequencial.

Um Stream não representa o dado em si — ele representa o mecanismo de transporte do dado.

Ele pode representar:

  • Arquivos em disco
  • Dados em memória
  • Comunicação de rede
  • Pipes entre processos
  • Compressão
  • Criptografia
  • Corpo de requisições HTTP

Classe base:

System.IO.Stream

Todos os streams concretos herdam dessa classe.


Modelo Mental Correto

Um Stream funciona como:

  • Um cursor sobre uma sequência de bytes
  • Um ponteiro que avança conforme os dados são consumidos
  • Um canal que pode ou não permitir reposicionamento

Pense nele como uma fita cassete:

  • Você pode avançar
  • Pode voltar se permitir rewind
  • Pode não permitir rewind
  • Pode acabar a fita

Estrutura Interna de um Stream

Propriedades essenciais:

stream.CanRead
stream.CanWrite
stream.CanSeek
stream.Position
stream.Length

CanRead

Indica se o stream permite leitura.

CanWrite

Indica se o stream permite escrita.

CanSeek

Indica se o stream permite reposicionamento.

Se false, o stream é forward-only.

Position

Indica o índice atual do cursor.

Length

Indica o tamanho total do fluxo.

Nem todo stream possui Length (ex: NetworkStream).


Leitura de Stream

Leitura é sempre baseada em buffer.

Leitura síncrona

byte[] buffer = new byte[1024];
int bytesRead = stream.Read(buffer, 0, buffer.Length);

O método retorna:

  • Quantidade de bytes lidos
  • 0 quando chega ao final

Leitura assíncrona

int bytesRead = await stream.ReadAsync(buffer);

Sempre prefira versão assíncrona para IO.


Escrita em Stream

Escrita também usa buffer.

byte[] data = Encoding.UTF8.GetBytes("Hello");

await stream.WriteAsync(data);

Seek e Reposicionamento

Se o stream for seekable:

stream.Position = 0;
stream.Seek(0, SeekOrigin.Begin);

Se CanSeek for false, isso lança exceção.

Streams de rede geralmente não suportam seek.


Tipos Fundamentais de Stream

MemoryStream

  • Armazena dados em memória
  • Totalmente seekable
  • Ideal para testes
  • Não representa cenário real de rede
var ms = new MemoryStream(bytes);

Cuidado com uso excessivo para grandes volumes de dados.


FileStream

  • Representa arquivo físico
  • Geralmente seekable
  • Pode ser grande
  • Deve ser corretamente descartado
using var fs = File.OpenRead("arquivo.txt");

NetworkStream

  • Usado em comunicação TCP
  • Não é seekable
  • Não possui Length conhecido
  • Só pode ser lido uma vez

BufferedStream

Adiciona buffer intermediário para melhorar performance.


CryptoStream

Aplica transformação criptográfica sobre outro stream.

Exemplo:

var cryptoStream = new CryptoStream(fileStream, encryptor, CryptoStreamMode.Write);

GZipStream

Aplica compressão ou descompressão sobre outro stream.


Encadeamento de Streams

Streams podem ser compostos.

Exemplo:

FileStream
→ GZipStream
→ CryptoStream

Cada camada transforma o fluxo de dados.

Isso é um padrão chamado decorator.


Ciclo de Vida e Dispose

Streams geralmente implementam:

IDisposable
IAsyncDisposable

Sempre use:

using var stream = ...;

ou

await using var stream = ...;

Falha em descartar pode causar:

  • File locks
  • Vazamento de memória
  • Exaustão de handles

O que significa "Stream Consumido"

Quando você lê até o fim:

while (await stream.ReadAsync(buffer) > 0) { }

O cursor vai para o final.

Se tentar ler novamente:

  • Retorna 0 bytes
  • Ou lança exceção
  • Ou falha dependendo do tipo

Se o stream não permitir seek, você não pode voltar.


Replayable vs Non-Replayable

Replayable:

  • MemoryStream
  • FileStream (seekable)

Non-Replayable:

  • NetworkStream
  • HttpRequest.Body
  • Streams de upload

Isso é crítico ao usar retry.


Streams e HttpClient

Quando você faz:

var content = new StreamContent(stream);
await client.SendAsync(request);

Internamente:

  • HttpClient chama CopyToAsync
  • O stream é lido completamente
  • O cursor vai para o fim

Se houver retry:

  • O stream pode já estar consumido
  • Pode estar fechado
  • Pode não permitir reposicionamento

Streams e Retry

Retry exige replay da operação.

Se o stream não for replayable:

  • O retry falha
  • Pode gerar upload incompleto
  • Pode lançar exceção

Forma segura:

await retry.ExecuteAsync(() =>
{
using var request = new HttpRequestMessage(...);
request.Content = new StreamContent(File.OpenRead("arquivo.txt"));
return client.SendAsync(request);
});

Cada tentativa cria novo stream.


LoadIntoBufferAsync

await content.LoadIntoBufferAsync();

Bufferiza conteúdo em memória.

Vantagens:

  • Permite replay

Desvantagens:

  • Pode consumir muita memória

Erros Comuns

Reutilizar Stream

Errado:

var content = new StreamContent(stream);
await client.SendAsync(request);
await client.SendAsync(request); // problema

Não resetar Position

Se for seekable:

stream.Position = 0;

Dispose antecipado

using var stream = File.OpenRead(...);
return new StreamContent(stream); // stream fechado antes do envio

Streams Forward-Only

Alguns streams:

  • Não sabem Length
  • Não permitem Seek
  • Só podem ser lidos uma vez

Exemplo típico:

HttpContext.Request.Body

Streams e Performance

Boas práticas:

  • Usar buffers reutilizáveis
  • Evitar converter para byte[] sem necessidade
  • Evitar carregar arquivo inteiro em memória
  • Usar operações async

Streams e Paralelismo

Streams normalmente não são thread-safe.

Não faça:

Task.Run(() => stream.Read(...));
Task.Run(() => stream.Read(...));

Pode corromper dados.


Streams Grandes

Para arquivos grandes:

  • Processar em blocos
  • Evitar MemoryStream
  • Evitar ToArray()

Criando Stream Customizado

class CustomStream : Stream
{
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;

public override int Read(byte[] buffer, int offset, int count)
{
return 0;
}

public override long Length => throw new NotSupportedException();
public override long Position { get; set; }

public override void Flush() {}
public override long Seek(long offset, SeekOrigin origin)
=> throw new NotSupportedException();
public override void SetLength(long value)
=> throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
}

Streams e Segurança

Streams podem:

  • Ser interceptados
  • Ser parcialmente enviados
  • Ser duplicados em retry incorreto

Sempre validar:

  • Idempotência
  • Integridade
  • Estado do cursor

Modelo Mental Final

Stream é:

  • Sequencial
  • Stateful
  • Mutável
  • Pode não ser reutilizável
  • Pode ser descartável
  • Pode não ser thread-safe

Qualquer mecanismo de retry ou reexecução deve considerar isso.


Servindo arquivos gigantes com .NET C# e System.IO.Stream


Abordagem Incorreta (Aloca memória desnecessariamente)

app.MapGet("/1", async (HttpContext context) =>
{
var mimeType = "video/mp4";
var path = @"../output.mp4";

var fileContent = await File.ReadAllBytesAsync(path);

return Results.File(fileContent, mimeType);
});

Problemas dessa abordagem

  • Lê o arquivo inteiro do disco.
  • Aloca um array de bytes na memória.
  • Cria uma instância de FileContentResult.
  • O framework pode alocar memória novamente internamente.
  • Alto consumo de RAM para arquivos grandes.

Abordagem Correta (Streaming direto do disco)

app.MapGet("/2", async (HttpContext context) =>
{
var mimeType = "video/mp4";
var path = @"../output.mp4";

var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
return Results.Stream(stream, contentType: mimeType);
});

Vantagens

  • Lê os dados apenas no momento da escrita no corpo da resposta.
  • Não carrega o arquivo inteiro na memória.
  • Evita alocação desnecessária.
  • Muito mais eficiente para arquivos grandes.

Abordagem Avançada (Controle Total do Streaming)

app.MapGet("/3", async (HttpContext context) =>
{
var path = @"../output.mp4";

using var stream = new FileStream(path, FileMode.Open, FileAccess.Read);

context.Response.ContentType = "video/mp4";
context.Response.ContentLength = stream.Length;

var buffer = new byte[1024 * 128]; // 128 KB
int bytesRead;

while ((bytesRead = await stream.ReadAsync(buffer)) > 0)
{
await context.Response.Body.WriteAsync(buffer, 0, bytesRead);
}
});

Características dessa abordagem

  • Única alocação de memória (buffer fixo de 128KB).
  • Controle granular sobre quantos bytes são lidos por iteração.
  • Trata o corpo da resposta como um WritableStream.
  • Permite transformação dos dados a nível de byte.
  • Ideal para cenários de alto desempenho.

Observações Importantes

  • Outros aspectos de rede influenciam a velocidade da transferência.

  • Usado sem cautela pode criar problemas complexos.

  • Streaming manual exige cuidado com:

    • Controle de buffer
    • Flush
    • Cancelamento
    • Backpressure

Conceito Central do Material

Evitar:

File.ReadAllBytesAsync()

Preferir:

FileStream + Streaming

Porque:

  • Arquivos gigantes não devem ser carregados totalmente na memória.
  • Streaming reduz drasticamente uso de RAM.
  • Escala melhor sob alta concorrência.