Async Zip no .NET 10: Criação ou Extração de Uma Linha
As operações de arquivo Zip em C# sempre foram possíveis através de System.IO.Compression, mas cada chamada era síncrona, o que significa que o thread era bloqueado até que o arquivo fosse totalmente escrito ou lido. .NET 10 muda isso com um conjunto de sobrecargas assíncronas que permitem criar, extrair e popular arquivos zip sem prender o thread chamador.
Este tutorial demonstra as novas sobrecargas de zip assíncronas no .NET 10, baseado no guia recente de Tim Corey. Vamos observar três abordagens detalhadas progressivas: uma linha para construir um arquivo, uma chamada única para descompactá-lo e um método manual para controle total, enquanto também examinamos um problema de escopo de using.
Configuração: Caminhos e a Pasta de Origem
[0:28 - 1:55] A configuração começa com um aplicativo de console direcionado a .NET 10 e uma única diretiva usando:
using System.IO.Compression;
using System.IO.Compression;
Três variáveis de string definem os caminhos usados durante a demonstração:
string sourceDirectory = @"C:\temp\test";
string destinationZipFile = @"C:\temp\archive.zip";
string destinationDirectory = @"C:\temp\extracted";
string sourceDirectory = @"C:\temp\test";
string destinationZipFile = @"C:\temp\archive.zip";
string destinationDirectory = @"C:\temp\extracted";
O prefixo de string literal (@) evita a necessidade de duplicar barras invertidas. sourceDirectory é a pasta a ser compactada. destinationZipFile é o caminho completo para o arquivo que será criado. destinationDirectory é onde o conteúdo será colocado ao ser extraído.
Um aviso prático: nunca aponte destinationZipFile para um caminho dentro de sourceDirectory. Escrever o zip na pasta que está sendo compactada causa um loop de leitura recursiva que quebra o processo.
A pasta de teste contém dois arquivos na raiz e um terceiro arquivo dentro de uma subpasta, o que importa ao explorar a opção includeBaseDirectory e o manuseio de caminhos relativos na abordagem manual.
Criando um Arquivo em Uma Linha
[2:35 - 4:20] A chamada de criação assíncrona é uma substituição direta para o síncrono ZipFile.CreateFromDirectory:
await ZipFile.CreateFromDirectoryAsync(
sourceDirectory,
destinationZipFile,
CompressionLevel.SmallestSize,
includeBaseDirectory: false);
await ZipFile.CreateFromDirectoryAsync(
sourceDirectory,
destinationZipFile,
CompressionLevel.SmallestSize,
includeBaseDirectory: false);
CompressionLevel.SmallestSize prioriza o menor tamanho de saída ao custo de um pouco mais de tempo de processamento. CompressionLevel.Fastest faz o inverso. Para a maioria dos cenários de desenvolvimento, a diferença é insignificante, mas em um servidor web processando muitos arquivos ao mesmo tempo, a troca precisa ser avaliada.
includeBaseDirectory controla se o nome do diretório de origem aparece como uma entrada raiz dentro do arquivo. Com false (o padrão), o zip é aberto diretamente para os arquivos. Passar true em vez disso, coloca uma pasta test na raiz, com os arquivos reais dentro dela. A maioria dos casos de uso se beneficia configurando isso para false.
Extraindo um Arquivo em Uma Linha
[4:45 - 5:55] A extração segue o mesmo padrão:
await ZipFile.ExtractToDirectoryAsync(
destinationZipFile,
destinationDirectory,
overwriteFiles: false);
await ZipFile.ExtractToDirectoryAsync(
destinationZipFile,
destinationDirectory,
overwriteFiles: false);
destinationDirectory é criado automaticamente se ainda não existir. O parâmetro overwriteFiles por padrão é false, o que lança uma exceção se qualquer arquivo no arquivo já existir no caminho de destino. Configure para true para substituir arquivos existentes sem aviso. Executar a extração duas vezes ilustra isso: a primeira passagem é bem-sucedida e cria a pasta, enquanto a segunda lança uma exceção, a menos que overwriteFiles: true seja especificado.
Compressão Seletiva: Adicionando Arquivos Um por Um
[6:15 - 9:50] A abordagem de uma linha compacta um diretório inteiro sem filtragem. Quando você precisa incluir apenas arquivos específicos, você constrói o arquivo manualmente usando um FileStream e ZipArchive:
await using FileStream zipStream = new FileStream(
destinationZipFile,
FileMode.Create,
FileAccess.Write,
FileShare.None,
bufferSize: 4096,
useAsync: true);
using ZipArchive archive = await ZipArchive.CreateAsync(
zipStream,
ZipArchiveMode.Create,
leaveOpen: false,
entryNameEncoding: null);
await using FileStream zipStream = new FileStream(
destinationZipFile,
FileMode.Create,
FileAccess.Write,
FileShare.None,
bufferSize: 4096,
useAsync: true);
using ZipArchive archive = await ZipArchive.CreateAsync(
zipStream,
ZipArchiveMode.Create,
leaveOpen: false,
entryNameEncoding: null);
Alguns parâmetros aqui valem a pena ser compreendidos. FileMode.Create sobrescreve qualquer arquivo existente nesse caminho. FileMode.CreateNew lança em vez disso se o arquivo já existir. FileShare.None bloqueia o arquivo exclusivamente enquanto o arquivo está sendo escrito, impedindo que outros processos o leiam ou escrevam durante a operação.
useAsync: true no FileStream habilita E/S assíncrona no nível do sistema operacional. Observe que definir isso sem realmente usar chamadas assíncronas a jusante pode desacelerar o processo significativamente, às vezes em até dez vezes. Porque o loop de escrita de entrada usa await, useAsync: true é a escolha correta aqui. Em um caminho de código síncrono, deixe no padrão false.
leaveOpen: false no ZipArchive informa para fechar e liberar o stream subjacente quando for descartado. entryNameEncoding: null mantém a codificação padrão, que a documentação recomenda deixar a menos que você tenha um motivo específico para alterá-la.
Com o arquivo pronto, recupere o conteúdo de origem e escreva cada entrada:
string[] files = Directory.GetFiles(sourceDirectory, "*", SearchOption.AllDirectories);
foreach (string filePath in files)
{
string relativePathAndName = Path.GetRelativePath(sourceDirectory, filePath);
await archive.CreateEntryFromFileAsync(filePath, relativePathAndName);
}
string[] files = Directory.GetFiles(sourceDirectory, "*", SearchOption.AllDirectories);
foreach (string filePath in files)
{
string relativePathAndName = Path.GetRelativePath(sourceDirectory, filePath);
await archive.CreateEntryFromFileAsync(filePath, relativePathAndName);
}
Directory.GetFiles com SearchOption.AllDirectories recupera arquivos de subpastas aninhadas, que é como a estrutura da subpasta é preservada no arquivo. O argumento do padrão ("*") é onde você filtra por extensão: "*.txt" limitaria o arquivo a arquivos de texto, por exemplo.
Path.GetRelativePath remove o prefixo absoluto de cada caminho de arquivo, deixando apenas a parte relativa a sourceDirectory. Isso é o que é armazenado como o nome da entrada dentro do zip, reproduzindo fielmente a hierarquia de subpastas. Se você passar Path.GetFileName(filePath), cada entrada vai para a raiz do zip, independentemente de sua localização original. Isso produz um arquivo plano, mas corre o risco de uma colisão de nomes se duas entradas compartilharem um nome de arquivo em diferentes subpastas.
A Armadilha do Using Escopado
[9:50 - 11:30] Se você tentar adicionar uma chamada de extração após o bloco de zip manual, você encontrará uma exceção de bloqueio de arquivo: The process cannot access the file because it is being used by another process. Isso acontece porque a declaração using em zipStream usa sintaxe de escopo de arquivo, o que significa que o stream permanece aberto até o final do arquivo, em vez de fechar na chave do bloco zip. A tentativa de extração é executada enquanto o bloqueio ainda está ativo.
Converter para uma forma com chave resolve isso:
// Before (file-scoped: stream stays open until end of file)
await using FileStream zipStream = new FileStream(...);
// After (block-scoped; stream released at the closing brace)
await using (FileStream zipStream = new FileStream(...))
{
// zip operations here
}
// stream is now closed; extraction can proceed safely
// Before (file-scoped: stream stays open until end of file)
await using FileStream zipStream = new FileStream(...);
// After (block-scoped; stream released at the closing brace)
await using (FileStream zipStream = new FileStream(...))
{
// zip operations here
}
// stream is now closed; extraction can proceed safely
Colocar o trabalho de compactação entre chaves e remover o ponto e vírgula final limita a vida útil do stream a esse escopo explícito. Uma vez que a execução deixa a chave de fechamento, o bloqueio é liberado e uma chamada de extração subsequente pode abrir o mesmo arquivo sem conflito.
Conclusão
[11:40 - end] Os comandos de uma linha lidam com os casos comuns: CreateFromDirectoryAsync para arquivar uma pasta inteira e ExtractToDirectoryAsync para descompactá-la. Quando você precisa de controle sobre quais arquivos entram no arquivo, a abordagem manual FileStream e ZipArchive oferece filtragem, renomeação e reescrita de caminhos no nível de entrada.
Para recapitular: adicione using System.IO.Compression, chame await ZipFile.CreateFromDirectoryAsync ou await ZipFile.ExtractToDirectoryAsync para casos diretos, e busque o caminho manual ZipArchive quando você precisar filtrar ou renomear entradas. Escopo seus blocos using com chaves se o stream precisar ser liberado antes que o código subsequente seja executado. Estas adições tornam os padrões async/await disponíveis durante todo o fluxo de trabalho zip em .NET 10.
Assista ao vídeo completo no canal do Tim Corey no YouTube para acompanhar a programação ao vivo.
