Async Zip no .NET 10: Criação ou Extração de Uma Linha
[[academy-video-youtube({"vid": "YgQ3ta6455A", "start_time": "0", "title": "Async Zip in .NET 10: One Line Create or Extract", "creator": "Tim Corey", "length": "~12m"})]]
As operações de arquivo Zip em C# sempre foram possíveis através de System.IO.Compression, mas cada chamada era síncrona, significando que o thread era bloqueado até que o arquivo estivesse 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 async zip no .NET 10, baseado no guia recente de Tim Corey. Veremos três abordagens cada vez mais detalhadas: uma linha única para criar um arquivo, uma única chamada para descompactá-lo e um método manual para controle total, enquanto também examinamos um problema comum com o 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 para compactar. destinationZipFile é o caminho completo para o arquivo que será criado. destinationDirectory é onde os conteúdos serão colocados quando extraídos.
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 async é 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 output 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 abre 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 ao definir 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 tem como padrão false, que lança uma exceção se qualquer arquivo no arquivo já existir no caminho de destino. Defina para true para substituir arquivos existentes silenciosamente. Executar a extração duas vezes ilustra isso: a primeira passa com sucesso 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 substitui qualquer arquivo existente nesse caminho. FileMode.CreateNew lança uma exceção se o arquivo já existir. FileShare.None bloqueia o arquivo exclusivamente enquanto o arquivo está sendo escrito, impedindo outros processos de lê-lo ou gravá-lo 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 na configuração padrão false.
leaveOpen: false no ZipArchive indica para fechar e liberar o fluxo subjacente quando ele for descartado. entryNameEncoding: null mantém a codificação padrão, que a documentação recomenda manter, 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, o que é como a estrutura de subpastas é preservada no arquivo. O argumento de 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) em vez disso, cada entrada será colocada na 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 zip manual, 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 fluxo 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 - fim] As linhas únicas tratam dos 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 caminho no nível de entrada.
Para recapitular: adicione using System.IO.Compression, chame await ZipFile.CreateFromDirectoryAsync ou await ZipFile.ExtractToDirectoryAsync para casos diretos e recorra ao caminho manual ZipArchive quando precisar filtrar ou renomear entradas. Enquadre seus blocos using com chaves se o fluxo 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.
