.NET 10での非同期Zip: ワンラインで作成または抽出
C#でのZIPファイル操作は常にSystem.IO.Compressionを通じて可能でしたが、すべての呼び出しは同期的であり、アーカイブが完全に書き込まれるか読み込まれるまでスレッドがブロックされます。 .NET 10はこれを変更し、一連の非同期オーバーロードによって、呼び出しスレッドをブロックせずにZipファイルを作成、抽出、ポピュレートできるようにします。
このウォークスルーは .NET 10 で新たに追加された非同期zipオーバーロードをティム・コーリーのガイドに基づいて紹介します。アーカイブを作成するワンライナー、解凍するシングルコール、完全な制御のための手動メソッド、そしてスコープされたusingの落とし穴を見ていきます。
セットアップ: パスとソースフォルダ
[0:28 - 1:55] セットアップは、.NET 10をターゲットとするコンソールアプリケーションと1つの使用ディレクティブから始まります。
using System.IO.Compression;using System.IO.Compression;3つのストリング変数がデモ全体で使用されるパスを定義します:
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";逐語的な文字列プレフィクス (@) はバックスラッシュを重ねる必要を回避します。 destinationZipFileは作成されるアーカイブのフルパスです。 destinationDirectoryは抽出された内容が格納される場所です。
1つの実用的な警告: sourceDirectoryの内部パスにポイントしてはいけません。 zipを生成するフォルダに書き込むと、プロセスを破る再帰的な読み取りループを引き起こします。
テストフォルダーにはルートに2つのファイルとサブフォルダー内に3つ目のファイルが含まれており、includeBaseDirectoryオプションを探索するときと手動アプローチでの相対パス処理において重要です。
ワンライナーでアーカイブを作成する
[2:35 - 4:20] 非同期の作成呼び出しは、同期的なZipFile.CreateFromDirectoryの直接ドロップインです。
await ZipFile.CreateFromDirectoryAsync(
sourceDirectory,
destinationZipFile,
CompressionLevel.SmallestSize,
includeBaseDirectory: false);await ZipFile.CreateFromDirectoryAsync(
sourceDirectory,
destinationZipFile,
CompressionLevel.SmallestSize,
includeBaseDirectory: false);CompressionLevel.Fastestはその逆です。 ほとんどの開発シナリオではその違いは無視できるものですが、多くのアーカイブを同時に処理するウェブサーバーでは、トレードオフは評価に値します。
falseでは、Zipは直接ファイルに開きます。 代わりにtestフォルダがルートに作成されます。 ほとんどのユースケースでこれをfalseに設定すると利点があります。
ワンライナーでアーカイブを抽出する
[4:45 - 5:55] 抽出は同じパターンに従います:
await ZipFile.ExtractToDirectoryAsync(
destinationZipFile,
destinationDirectory,
overwriteFiles: false);await ZipFile.ExtractToDirectoryAsync(
destinationZipFile,
destinationDirectory,
overwriteFiles: false);存在しない場合はfalseがデフォルトであり、ターゲットパスにアーカイブ内のファイルが既に存在する場合、例外をスローします。 既存のファイルを音もなく置き換えるには、これをtrueに設定します。 抽出を2回実行するとこれが例示されます: 最初の通過は成功し、フォルダーを作成します。一方で2回目はoverwriteFiles: trueを指定しない限り例外をスローします。
選択的Zip化: ファイルを一つずつ追加
[6:15 - 9:50] ワンライナーアプローチはフィルタリングなしでディレクトリ全体をZip化します。 特定のファイルのみを含める必要がある場合、FileStreamと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);ここでいくつかのパラメータは理解する価値があります。 FileMode.Createはそのパスに既存するファイルを上書きします。 代わりにファイルが既に存在している場合、FileMode.CreateNewは例外をスローします。 FileShare.Noneはアーカイブが書き込まれる間、ファイルを排他的にロックし、他のプロセスが途中で読み書きするのを防ぎます。
FileStreamでのuseAsync: trueはOSレベルで非同期I/Oを可能にします。 これを設定しても下流で実際に非同期呼び出しを使用しないと、プロセスが大幅に遅くなることがあります。時には10倍にもなります。 エントリ書き込みループはuseAsync: trueが正しい選択です。 同期コードパスでは、デフォルトのfalseのままにしてください。
ZipArchiveでのleaveOpen: falseは、破棄されたときに基になるストリームを閉じてフラッシュするよう指示します。 デフォルトのエンコーディングを保持するentryNameEncoding: nullは、特定の理由がない限り変更しないように文書では推奨されています。
アーカイブの準備が整ったら、ソース内容を取得して各エントリーを書き込みます。
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);
}SearchOption.AllDirectoriesと共に使用すると、ネストされたサブフォルダーからファイルを取得し、このようにしてサブフォルダー構造がアーカイブで維持されます。パターン引数 ("*") は拡張子でフィルタリングする場所であり、例えば"*.txt"はアーカイブをテキストファイルに限定します。
sourceDirectoryに相対する部分だけを残します。 これが、zip内にエントリー名として保存され、サブフォルダ階層が忠実に再現されるものです。 代わりにPath.GetFileName(filePath)を渡すと、元の場所に関係なくすべてのエントリがZipのルートに配置されます。 これはフラットなアーカイブを生成しますが、異なるサブフォルダーでファイル名を共有しているエントリーがあると名前の衝突が発生する可能性があります。
Scoped Using Pitfall
[9:50 - 11:30] 手動Zipブロックの後に抽出呼び出しを追加しようとすると、ファイルロック例外が発生します: The process cannot access the file because it is being used by another process。 これはzipStreamに関するusingステートメントがファイルスコープの構文を使用しているために発生し、ストリームがZipブロックのブラケットで閉じるのではなく、ファイルの最後まで開いたままになるということです。 ロックがまだ保持されている間に抽出試行が行われます。
ブレース形式に変換することで、この問題が解決します。
// 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 safelyzip作業をブレースで囲み、末尾のセミコロンを削除することで、ストリームの寿命をその明示的なスコープに制限します。 実行が閉じる括弧を出たら、ロックが解除され、後続の抽出呼び出しで同じファイルを無矛盾で開けるようになります。
結論
[11:40 - end] ワンライナーは一般的なケースを処理します: フォルダー全体をアーカイブするためのExtractToDirectoryAsync。 特定のファイルをアーカイブに含める制御が必要な場合、手動のZipArchiveアプローチはフィルタリング、リネーミング、およびパスの書き換えをエントリレベルで可能にします。
まとめます: ZipArchiveパスを使用します。 後続のコードを実行する前にストリームを解放する必要がある場合、usingブロックをブラケットでスコープします。 これらの追加により、.NET 10全体でのフルzipワークフローでasync/awaitパターンが利用可能です。
ライブコーディングをフォローするには、Tim CoreyのYouTube動画を完全に視聴してください。

