C# ZipFile
last modified October 18, 2026
In this article we use ZipFile to create, read, and extract ZIP files
in C#.
ZIP
ZIP is an archive file format that supports lossless data compression. A ZIP file may contain one or more files or directories that may have been compressed. Each file is compressed separately, and the archive has a central directory at its end, so a single entry can be read without unpacking the rest.
The System.IO.Compression namespace is part of the .NET base class
library, so no package is needed. It contains two levels:
ZipFile- static helper methods for creating, extracting, and opening archives that live in filesZipArchiveandZipArchiveEntry- classes for working with the content of an archive, over any stream
In the examples, we work in a directory with the following layout.
$ find data -type f data/a.jpg data/big.txt data/sub/note.txt data/words.txt data/words2.txt
C# ZipFile.CreateFromDirectory
The ZipFile.CreateFromDirectory method creates a zip archive that
contains the files and directories from the specified directory.
using System.IO.Compression;
string dirName = "data";
string zipName = "archive.zip";
if (File.Exists(zipName))
{
File.Delete(zipName);
}
ZipFile.CreateFromDirectory(dirName, zipName);
Console.WriteLine($"{zipName}: {new FileInfo(zipName).Length} bytes");
// the overload with options
if (File.Exists("archive2.zip"))
{
File.Delete("archive2.zip");
}
ZipFile.CreateFromDirectory(dirName, "archive2.zip", CompressionLevel.SmallestSize, includeBaseDirectory: true);
Console.WriteLine($"archive2.zip: {new FileInfo("archive2.zip").Length} bytes");
In the example, we create two ZIP files from a directory.
if (File.Exists(zipName))
{
File.Delete(zipName);
}
We check whether the archive already exists, and delete it if it does. The method
throws an IOException if the destination file exists.
ZipFile.CreateFromDirectory(dirName, zipName);
The parameters are the source directory and the name of the ZIP file. All subdirectories are included, and the entry names are relative to the source directory.
ZipFile.CreateFromDirectory(dirName, "archive2.zip", CompressionLevel.SmallestSize, includeBaseDirectory: true);
The longer overload selects the compression level and whether the name of the
directory itself (data/) is the first segment of every entry name.
The levels are Optimal (default), Fastest,
SmallestSize, and NoCompression. For small, already
compressed, or random data, a higher level may even make the result larger, as
the sizes below show.
$ dotnet run archive.zip: 541 bytes archive2.zip: 591 bytes
The second archive is bigger, because it contains the additional base directory entry names.
C# ZipFile include specified files
In the next example, we choose which files we include in the archive.
using System.IO.Compression;
string zipName = "texts.zip";
if (File.Exists(zipName))
{
File.Delete(zipName);
}
var files = Directory.GetFiles("data", "*.txt", SearchOption.AllDirectories);
using (var archive = ZipFile.Open(zipName, ZipArchiveMode.Create))
{
foreach (var file in files)
{
// keep the relative path, using forward slashes inside the archive
string entryName = Path.GetRelativePath("data", file).Replace('\\', '/');
archive.CreateEntryFromFile(file, entryName, CompressionLevel.Optimal);
}
}
using var check = ZipFile.OpenRead(zipName);
foreach (var e in check.Entries)
{
Console.WriteLine(e.FullName);
}
The program takes all text files from the specified directory and its subdirectories and creates an archive from them.
var files = Directory.GetFiles("data", "*.txt", SearchOption.AllDirectories);
With Directory.GetFiles, we find all files that match the given
pattern. SearchOption.AllDirectories includes the subdirectories.
using (var archive = ZipFile.Open(zipName, ZipArchiveMode.Create))
We open the ZIP archive in the ZipArchiveMode.Create mode with
ZipFile.Open. The archive is written when it is disposed, so the
using block must end before we read the file.
string entryName = Path.GetRelativePath("data", file).Replace('\\', '/');
archive.CreateEntryFromFile(file, entryName, CompressionLevel.Optimal);
We add files with CreateEntryFromFile. The second argument is the
name of the entry inside the archive. We keep the relative path, so that
sub/note.txt stays in its subdirectory. Entry names should use forward
slashes, which is what the ZIP format specifies.
$ dotnet run big.txt words2.txt words.txt sub/note.txt
The order of the entries depends on the order in which the file system returns the files.
C# ZipFile.ExtractToDirectory
The ZipFile.ExtractToDirectory method extracts all the files in the
specified zip archive to the given directory. The directory is created if it does
not exist.
using System.IO.Compression;
string dirName = "data2";
string zipName = "archive.zip";
if (Directory.Exists(dirName))
{
Directory.Delete(dirName, true);
}
ZipFile.ExtractToDirectory(zipName, dirName);
Console.WriteLine(string.Join(", ", Directory.GetFiles(dirName, "*", SearchOption.AllDirectories)
.Select(f => Path.GetRelativePath(dirName, f)).Order()));
// a second extraction fails because the files exist
try
{
ZipFile.ExtractToDirectory(zipName, dirName);
}
catch (IOException e)
{
Console.WriteLine("the second extraction failed: " + e.GetType().Name);
}
// overwriteFiles: true replaces them
ZipFile.ExtractToDirectory(zipName, dirName, overwriteFiles: true);
Console.WriteLine("extracted again");
In the example, we extract the contents of a ZIP file to a directory, try to do it again, and then use the overload that allows overwriting.
ZipFile.ExtractToDirectory(zipName, dirName);
The first extraction goes into an empty directory. The listing shows that the subdirectory was recreated.
catch (IOException e)
By default, the method does not overwrite existing files; it throws an
IOException instead.
ZipFile.ExtractToDirectory(zipName, dirName, overwriteFiles: true);
The overload with the overwriteFiles parameter replaces the existing
files. This is simpler and safer than deleting the directory first, as the older
version of this tutorial did.
$ dotnet run a.jpg, big.txt, sub/note.txt, words.txt, words2.txt the second extraction failed: IOException extracted again
C# ZipFile read contents
In the next example, we read the contents of a ZIP file without extracting it.
using System.IO.Compression;
using var archive = ZipFile.OpenRead("archive.zip");
Console.WriteLine($"{"Name",-16} {"Size",8} {"Packed",8} {"Modified"}");
foreach (ZipArchiveEntry entry in archive.Entries.Where(e => e.Name.EndsWith(".txt")).OrderBy(e => e.FullName))
{
Console.WriteLine($"{entry.FullName,-16} {entry.Length,8} {entry.CompressedLength,8} {entry.LastWriteTime:yyyy-MM-dd}");
}
// a directory entry has an empty Name
Console.WriteLine(archive.Entries.Count(e => e.Name == ""));
With ZipFile.OpenRead, we open a zip archive for reading. We list the
text entries with their uncompressed and compressed sizes.
foreach (ZipArchiveEntry entry in archive.Entries.Where(e => e.Name.EndsWith(".txt")).OrderBy(e => e.FullName))
The Entries property retrieves all the entries of an archive. Each
ZipArchiveEntry has a Name (the file name),
FullName (the path inside the archive), Length (the
uncompressed size), CompressedLength, and LastWriteTime.
Console.WriteLine(archive.Entries.Count(e => e.Name == ""));
An entry for a directory has the full name ending with a slash and an empty
Name. CreateFromDirectory does not write entries for
directories that contain files, so the count is zero here.
$ dotnet run Name Size Packed Modified big.txt 3000 21 2026-10-18 sub/note.txt 7 9 2026-10-18 words.txt 15 17 2026-10-18 words2.txt 4 6 2026-10-18 0
The 3000-byte file of identical characters compresses to 21 bytes, while for tiny files, the compressed form is slightly larger because of the compression overhead.
C# ZipFile read and extract a single entry
We do not have to extract the whole archive. We can find an entry by name, read it directly, or extract only that one.
using System.IO.Compression;
using var archive = ZipFile.OpenRead("archive.zip");
ZipArchiveEntry? entry = archive.GetEntry("words.txt");
if (entry is null)
{
Console.WriteLine("entry not found");
return;
}
using Stream stream = entry.Open();
using var reader = new StreamReader(stream);
Console.WriteLine(reader.ReadToEnd());
// extract a single entry to disk
entry.ExtractToFile("words-copy.txt", overwrite: true);
Console.WriteLine(File.ReadAllText("words-copy.txt").Length);
The program reads one text file from the archive and extracts it to a new file.
ZipArchiveEntry? entry = archive.GetEntry("words.txt");
The GetEntry method looks an entry up by its full name, and returns
null if there is none. The name comparison is case-sensitive.
using Stream stream = entry.Open(); using var reader = new StreamReader(stream);
The Open method returns a decompressing stream, which we can wrap in
a StreamReader to read the text. Only this entry is decompressed.
entry.ExtractToFile("words-copy.txt", overwrite: true);
ExtractToFile writes the entry to the given path and also restores
its last write time.
$ dotnet run old falcon owl 15
C# ZipArchive create, update and delete entries
The ZipArchiveMode.Update mode lets us add, change, and delete entries
of an existing archive. The CreateEntry method creates an empty entry
whose content we write through a stream, which is how we put data that is not in a
file into an archive.
using System.IO.Compression;
using System.Text;
string zipName = "notes.zip";
if (File.Exists(zipName))
{
File.Delete(zipName);
}
// create entries from memory
using (var archive = ZipFile.Open(zipName, ZipArchiveMode.Create))
{
ZipArchiveEntry entry = archive.CreateEntry("notes/hello.txt", CompressionLevel.Optimal);
using var writer = new StreamWriter(entry.Open(), Encoding.UTF8);
writer.WriteLine("Hello from memory");
writer.WriteLine("žltý kôň");
}
// add an entry to an existing archive
using (var archive = ZipFile.Open(zipName, ZipArchiveMode.Update))
{
archive.CreateEntryFromFile("data/words2.txt", "words2.txt");
// delete an entry
archive.GetEntry("notes/hello.txt")?.Delete();
}
using var read = ZipFile.OpenRead(zipName);
Console.WriteLine(string.Join(", ", read.Entries.Select(e => e.FullName)));
The program creates an archive with an entry produced from memory, and then opens the archive for updating.
ZipArchiveEntry entry = archive.CreateEntry("notes/hello.txt", CompressionLevel.Optimal);
using var writer = new StreamWriter(entry.Open(), Encoding.UTF8);
We create the entry and write the text into its stream. A path in the entry name creates the directory structure implicitly.
using (var archive = ZipFile.Open(zipName, ZipArchiveMode.Update))
{
archive.CreateEntryFromFile("data/words2.txt", "words2.txt");
archive.GetEntry("notes/hello.txt")?.Delete();
}
In the update mode, we add a file and delete the entry created before. The changes are applied to the file when the archive is disposed. In this mode, the archive is rewritten, which makes it unsuitable for big archives with frequent small changes.
$ dotnet run words2.txt
C# ZIP archive in memory and async
The ZipArchive class works with any stream, so we can create and read
archives in memory, for instance to send them over a network or return them from a
web endpoint. Since .NET 10, ZipFile also has asynchronous methods.
using System.IO.Compression;
// an archive in memory, without any files
var ms = new MemoryStream();
using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true))
{
var e = archive.CreateEntry("a.txt");
using var w = new StreamWriter(e.Open());
w.Write(new string('x', 1000));
}
Console.WriteLine($"zip in memory: {ms.Length} bytes");
ms.Position = 0;
using var read = new ZipArchive(ms, ZipArchiveMode.Read);
var entry = read.Entries[0];
Console.WriteLine($"{entry.Name}: {entry.Length} -> {entry.CompressedLength} bytes");
// async extraction (.NET 10)
Directory.CreateDirectory("async-out");
await ZipFile.ExtractToDirectoryAsync("archive.zip", "async-out", overwriteFiles: true);
Console.WriteLine(Directory.GetFiles("async-out").Length > 0);
We create an archive in a memory stream, read it back, and extract a file archive asynchronously.
using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true))
The leaveOpen argument keeps the memory stream open after the archive
is disposed. Disposing the archive writes the central directory, which is why we
use a block and read the stream only afterwards.
await ZipFile.ExtractToDirectoryAsync("archive.zip", "async-out", overwriteFiles: true);
The asynchronous variants, CreateFromDirectoryAsync,
ExtractToDirectoryAsync, and OpenReadAsync, avoid
blocking a thread during disk access. On earlier versions of .NET, use
Task.Run or the synchronous methods.
$ dotnet run zip in memory: 126 bytes a.txt: 1000 -> 18 bytes True
C# ZIP and security: zip slip
Entry names are arbitrary strings chosen by whoever created the archive. A
malicious archive can contain a name such as ../../evil.txt that tries
to write outside the target directory (the "zip slip" vulnerability).
ZipFile.ExtractToDirectory protects against it, but if we write the
extraction code ourselves, we must check the destination path.
using System.IO.Compression;
// a malicious archive may contain entries such as ../../evil.txt ("zip slip")
string zipName = "evil.zip";
if (File.Exists(zipName)) File.Delete(zipName);
using (var archive = ZipFile.Open(zipName, ZipArchiveMode.Create))
{
var e = archive.CreateEntry("../../evil.txt");
using var w = new StreamWriter(e.Open());
w.Write("gotcha");
}
try
{
ZipFile.ExtractToDirectory(zipName, "safe-out", overwriteFiles: true);
}
catch (IOException e)
{
Console.WriteLine("blocked: " + e.Message);
}
// when extracting entries manually, check the destination ourselves
string destRoot = Path.GetFullPath("manual-out") + Path.DirectorySeparatorChar;
using var zip = ZipFile.OpenRead(zipName);
foreach (var entry in zip.Entries)
{
string target = Path.GetFullPath(Path.Combine(destRoot, entry.FullName));
if (!target.StartsWith(destRoot, StringComparison.Ordinal))
{
Console.WriteLine($"skipped suspicious entry: {entry.FullName}");
continue;
}
entry.ExtractToFile(target, true);
}
The program creates an archive with a dangerous entry and tries to extract it.
string target = Path.GetFullPath(Path.Combine(destRoot, entry.FullName)); if (!target.StartsWith(destRoot, StringComparison.Ordinal))
We resolve the full path of the destination, which collapses the ..
segments, and verify that it still begins with the destination directory. The root
ends with a directory separator, so a sibling directory such as
manual-out-evil cannot pass the test.
$ dotnet run blocked: Extracting Zip entry would have resulted in a file outside the specified destination directory. skipped suspicious entry: ../../evil.txt
Another risk is the decompression bomb, a small archive that expands to an enormous
size. When processing untrusted archives, check the declared Length
of entries and limit the total size, while remembering that a declared length can
lie, so also limit the number of bytes actually read.
Source
In this article we have worked with ZIP files in C#.
Author
List all C# tutorials.