ZipFile.CreateFromDirectory 不存档空文件夹,是设计行为而非 bug;需改用 ZipArchive 手动添加以“/”结尾的目录条目,或选用 SharpZipLib 等支持显式目录条目的第三方库。
ZipFile.CreateFromDirectory 会丢空文件夹,这是设计如此
不是你代码写错了,是 zipfile.createfromdirectory 根本不存档空目录——它只遍历文件,遇到空文件夹直接跳过。微软文档里没明说“不支持”,但行为就是如此,连 .net 6/7/8 都没改。
如果你的项目依赖空文件夹结构(比如 ASP.NET 的 wwwroot 下留着空 images 目录供前端上传,或测试用的占位目录),直接调用这个方法会导致解压后目录结构残缺。
手动构造 ZIP 流 + 添加空目录条目(最稳方案)
必须绕过 ZipFile.CreateFromDirectory,改用 ZipArchive + ZipArchiveMode.Create,自己控制每个条目。关键点在于:空文件夹要以 / 结尾的路径作为 ZipArchiveEntry 名称,并且不能写入任何内容。
- 用
Directory.GetDirectories(root, "*", SearchOption.AllDirectories)拿全路径,再转成相对于root的相对路径 +/ - 每个空目录路径调用
archive.CreateEntry(relativePath + "/"),不调.Open()、不写数据 - 文件照常用
CreateEntryFromFile或CreateEntry+Open().Write() - 注意路径分隔符统一用
/(ZIP 规范要求),别用\,否则某些解压工具会当普通文件处理
示例片段:
using (var archive = ZipFile.Open(outputPath, ZipArchiveMode.Create))
{
// 先加所有空目录(含子目录)
foreach (var dir in Directory.GetDirectories(sourceDir, "*", SearchOption.AllDirectories))
{
var rel = Path.GetRelativePath(sourceDir, dir).Replace("\", "/");
archive.CreateEntry(rel + "/");
}
// 再加所有文件
foreach (var file in Directory.GetFiles(sourceDir, "*.*", SearchOption.AllDirectories))
{
var rel = Path.GetRelativePath(sourceDir, file).Replace("\", "/");
archive.CreateEntryFromFile(file, rel);
}
}
第三方库 SharpZipLib 能省事但得权衡
SharpZipLib 的 ZipOutputStream 支持显式写入目录条目(PutNextEntry + Name 以 / 结尾),逻辑更直白,也不依赖 .NET 版本。
- 不需要提前扫描两次目录,边遍历边写即可
- 但引入新依赖,如果项目已禁用 NuGet 外部包,就得卡住
-
ZipOutputStream默认不压缩空目录,仍需手动PutNextEntry并设置IsDirectory属性(v1.3+)或靠命名约定 - 生成的 ZIP 在 macOS 上解压时偶尔有权限问题(非致命,但 CI 日志里可能报 warning)
别碰 ZipFile.ExtractToDirectory 的反向陷阱
有人想“先解压再重压”来补空目录,但 ZipFile.ExtractToDirectory 同样不还原空文件夹——它只建有文件的路径。也就是说,源 ZIP 里没空目录,解出来肯定也没有;你再压回去,还是没有。
更麻烦的是:如果目标目录已存在同名空文件夹,而 ZIP 里没对应条目,ExtractToDirectory 不会删它,导致残留脏数据。所以“解-改-压”流程不可靠,纯属绕远路。
真正要命的细节是:有些构建脚本用 dotnet publish 输出带空目录,但后续用 ZipFile.CreateFromDirectory 打包时就悄悄丢了——这种问题在 CI 环境里难复现,因为本地开发机上目录可能被其他操作“意外填充”过。










