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.