Crear y extraer archivos ZIP en C# con System.IO.Compression

Comprimir una carpeta en un ZIP, leer un archivo de dentro sin extraer nada o descomprimir lo que te envían: .NET lo trae todo en System.IO.Compression, sin paquetes. Y con una protección contra los ZIP maliciosos que conviene conocer.

Comprimir una carpeta

Todo está en System.IO.Compression, que viene con .NET. La forma más corta de crear un ZIP es comprimir una carpeta entera:

using System.IO.Compression;

// Una carpeta de ejemplo con tres archivos
Directory.CreateDirectory("informe/datos");
File.WriteAllText("informe/resumen.txt", "Ventas de octubre: 1,240 unidades.\n");
File.WriteAllText("informe/datos/ventas.csv", string.Concat(Enumerable.Repeat("2026-10-01,Café,3,4.50\n", 200)));
File.WriteAllText("informe/datos/notas.txt", "Revisar el stock de té.\n");

File.Delete("informe.zip");
ZipFile.CreateFromDirectory("informe", "informe.zip");

using var zip = ZipFile.OpenRead("informe.zip");
foreach (var entrada in zip.Entries.OrderBy(e => e.FullName))
{
    Console.WriteLine($"{entrada.FullName,-20} {entrada.Length,6} bytes -> {entrada.CompressedLength,4} comprimido");
}
salidadatos/notas.txt          25 bytes ->   27 comprimido
datos/ventas.csv       4800 bytes ->   76 comprimido
resumen.txt              35 bytes ->   37 comprimido

CreateFromDirectory recorre la carpeta con sus subcarpetas, y cada archivo queda como una entrada con su ruta relativa. Para mirar lo que hay dentro, ZipFile.OpenRead da un ZipArchive con su lista Entries: el tamaño original y el comprimido. El CSV, que repite la misma línea 200 veces, se queda en casi nada; los textos cortos, en cambio, ocupan incluso un poco más que el original, porque la compresión añade su propia información. Las entradas usan siempre / como separador, también en Windows.

Leer una entrada sin extraer nada

using System.IO.Compression;

// Una carpeta de ejemplo con tres archivos
Directory.CreateDirectory("informe/datos");
File.WriteAllText("informe/resumen.txt", "Ventas de octubre: 1,240 unidades.\n");
File.WriteAllText("informe/datos/ventas.csv", string.Concat(Enumerable.Repeat("2026-10-01,Café,3,4.50\n", 200)));
File.WriteAllText("informe/datos/notas.txt", "Revisar el stock de té.\n");

File.Delete("informe.zip");
ZipFile.CreateFromDirectory("informe", "informe.zip");

using var zip = ZipFile.OpenRead("informe.zip");
var resumen = zip.GetEntry("resumen.txt")!;
using var lector = new StreamReader(resumen.Open());
Console.Write(lector.ReadToEnd());

Console.WriteLine(zip.GetEntry("no-existe.txt") is null);
salidaVentas de octubre: 1,240 unidades.
True

GetEntry busca una entrada por su ruta (o devuelve null), y Open() la abre como un Stream que se descomprime al leerlo, sin crear archivos en el disco.

Crear un ZIP entrada a entrada

using System.IO.Compression;

File.Delete("copia.zip");
using (var zip = ZipFile.Open("copia.zip", ZipArchiveMode.Create))
{
    var nota = zip.CreateEntry("leeme.txt", CompressionLevel.SmallestSize);
    using var escritor = new StreamWriter(nota.Open());
    escritor.WriteLine("Copia generada por el programa.");
}

using (var zip = ZipFile.Open("copia.zip", ZipArchiveMode.Update))
{
    File.WriteAllText("config.json", "{ \"version\": 2 }");
    zip.CreateEntryFromFile("config.json", "ajustes/config.json");
}

using var final = ZipFile.OpenRead("copia.zip");
Console.WriteLine(string.Join(", ", final.Entries.Select(e => e.FullName)));
salidaleeme.txt, ajustes/config.json

Con ZipArchiveMode.Create se escribe un ZIP nuevo; con Update, se añade a uno que ya existe. CreateEntry crea una entrada vacía para escribir en ella lo que quieras, y CreateEntryFromFile mete un archivo del disco con el nombre que le digas, carpetas incluidas.

Extraer

using System.IO.Compression;

// Una carpeta de ejemplo con tres archivos
Directory.CreateDirectory("informe/datos");
File.WriteAllText("informe/resumen.txt", "Ventas de octubre: 1,240 unidades.\n");
File.WriteAllText("informe/datos/ventas.csv", string.Concat(Enumerable.Repeat("2026-10-01,Café,3,4.50\n", 200)));
File.WriteAllText("informe/datos/notas.txt", "Revisar el stock de té.\n");

File.Delete("informe.zip");
ZipFile.CreateFromDirectory("informe", "informe.zip");
if (Directory.Exists("destino")) Directory.Delete("destino", recursive: true);

ZipFile.ExtractToDirectory("informe.zip", "destino");
Console.WriteLine(string.Join(", ", Directory.GetFiles("destino", "*", SearchOption.AllDirectories).Order()));

try
{
    ZipFile.ExtractToDirectory("informe.zip", "destino");
}
catch (IOException)
{
    Console.WriteLine("La segunda vez falla: los archivos ya existen");
}

ZipFile.ExtractToDirectory("informe.zip", "destino", overwriteFiles: true);
Console.WriteLine("Con overwriteFiles: true, los sustituye");
salidadestino/datos/notas.txt, destino/datos/ventas.csv, destino/resumen.txt
La segunda vez falla: los archivos ya existen
Con overwriteFiles: true, los sustituye

Por omisión, ExtractToDirectory se niega a sobrescribir: si un archivo ya existe lanza IOException, y para entonces puede haber extraído una parte. Si quieres sustituirlos, dilo con overwriteFiles: true.

Los ZIP que intentan salirse de la carpeta

Un ZIP puede traer entradas con rutas como ../fuera.txt. Un programa que las extrajera sin mirar escribiría fuera de la carpeta de destino: un fallo de seguridad conocido como Zip Slip, con el que un archivo descargado puede sobrescribir lo que quiera. Este programa fabrica uno así a propósito:

using System.IO.Compression;

File.Delete("raro.zip");
using (var zip = ZipFile.Open("raro.zip", ZipArchiveMode.Create))
{
    using var w = new StreamWriter(zip.CreateEntry("../fuera.txt").Open());
    w.Write("esto no debería salir de la carpeta");
}

try
{
    ZipFile.ExtractToDirectory("raro.zip", "seguro");
}
catch (IOException ex)
{
    Console.WriteLine(ex.Message);
}
Console.WriteLine($"¿Existe ../fuera.txt? {File.Exists("../fuera.txt")}");
salidaExtracting Zip entry would have resulted in a file outside the specified destination directory.
¿Existe ../fuera.txt? False
ExtractToDirectory te protege; tu propio bucle, no. Los métodos de .NET comprueban la ruta y se niegan, como se ve arriba. Pero si recorres las entradas y escribes cada una con Path.Combine(destino, entrada.FullName), esa comprobación desaparece. En ese caso, calcula la ruta completa con Path.GetFullPath y verifica que empieza por la carpeta de destino antes de escribir.

Todo el código de esta página se compiló con el .NET SDK 10.0.403 (C# 14) y se ejecutó con .NET 10.0.14 en un contenedor limpio antes de publicarla; las salidas y los mensajes del compilador están copiados de esa ejecución.