Magicodes.IE.IO:零依赖、流式低分配的 .NET Excel 读写库
目录
- 为什么又造一个 Excel 库?
- 核心设计哲学
- 五分钟上手
- 与其他 Excel 库的对比
- 高级功能速览
- 与老 Magicodes.IE.Excel 的区别
- AOT / NativeAOT 支持
- 迁移指南
- 总结
为什么又造一个 Excel 库?
.NET 生态里的 Excel 读写方案不少——EPPlus、ClosedXML、MiniExcel、NPOI,都是成熟的好库,各有各的适用场景。但对于类库作者来说,选哪个就意味着把它的依赖链带进你的项目。EPPlus 和 ClosedXML 各自拖着一串 NuGet 包——每多一个依赖就多一份升级负担、多一个版本冲突风险。
Magicodes.IE 团队在维护老 Magicodes.IE.Excel(基于 EPPlus)的过程中吃够了这个苦,于是从头实现了一个 零运行时依赖、流式低内存、多 TFM 覆盖 的 Excel I/O 引擎——Magicodes.IE.IO。
dotnet add package Magicodes.IE.IO
支持 netstandard2.0 / net6.0 / net8.0 / net10.0。net6.0 及以上只依赖 BCL。
核心设计哲学
1. 零运行时依赖
不依赖 EPPlus、不依赖 ClosedXML、不依赖任何第三方 Excel 库。net6.0+ 目标只引用 BCL,netstandard2.0 只包含少量兼容性 polyfill(System.Text.Encoding.CodePages 等)。
这意味着:
- 不会引入第三方 Excel 库的版本冲突
- 你的类库可以放心引用
Magicodes.IE.IO,不会让下游被迫引入 Excel 依赖 - NuGet 包体积极小,审计面窄
2. 流式低内存写入
核心写入路径尽量少分配托管内存。我们不构建 DOM 树、不把整个 workbook 加载到内存再序列化——而是边算边写:
| 场景 | 10 万行 4 列 | 分配量 |
|---|---|---|
同步 Write(Stream) |
~38 ms | ~72 KB(固定) |
异步 WriteAsync(Stream) |
~39 ms | ~92 KB |
便利层 ToBytes() |
~39 ms | ~8.5 MB(物化为 byte[]) |
换句话说:同步流式写的 ~72KB 是固定开销(内部缓冲、ZIP 头、共享字符串字典),与行数无关。1 万行是 68KB,10 万行是 72KB——只多了 4.5KB。导 100 万行,内存也涨不上去。
ToBytes() 因需把整个文件物化成 byte[],分配随数据量线性增长(10 万行约 8.5 MB),这是设计内权衡。大数据请用 Write(Stream)。
3. 完整的异步支持
// 写:支持 IEnumerable<T> 和 IAsyncEnumerable<T>
await Xlsx.WriteAsync(stream, data); // 已物化集合异步写入
await Xlsx.WriteAsync(stream, GetAsyncData()); // IAsyncEnumerable 边查边写
await Xlsx.WriteAsync("/tmp/orders.xlsx", orders); // 文件路径便利重载
// 读:支持同步枚举和异步枚举
var rows = Xlsx.Read<Order>(stream).ToList();
await foreach (var o in Xlsx.ReadAsync<Order>(stream))
Console.WriteLine(o.OrderNo);
IAsyncEnumerable 路径配合 EF Core 的 AsAsyncEnumerable(),可以从数据库流式读取并直接写入 xlsx,不需要先把数据全部读到内存。
4. 正确性优先
每个导出的 xlsx 都经过三层格式校验:
- 必需部件齐全(
[Content_Types].xml、workbook.xml、sheet1.xml、styles.xml、_rels) - 所有 XML/rels 均为 well-formed
- OpenXML 包关系图完整(
.rels引用的每个 target 真实存在)
同时支持 1900/1904 双日期系统的正确读取(自动归一化)。
五分钟上手
零配置导出
// 写文件
Xlsx.Write("/tmp/orders.xlsx", orders);
// 写流(响应给浏览器)
Xlsx.Write(Response.Body, orders);
// 直接拿到 byte[]
var bytes = Xlsx.ToBytes(orders);
表头 = 属性名,列序 = 声明序,自动识别 string / number / DateTime / bool / enum / struct / record。
Fluent Profile 配置
var bytes = Xlsx.ToBytes(orders, p => p
.Sheet("订单表")
.Column(x => x.OrderNo, c => c.WithName("订单号").WithWidth(30))
.Column(x => x.Amount, c => c.WithFormat("0.00"))
.Ignore(x => x.CreatedAt)
.WithFreezeHeader(true));
多 Sheet
Xlsx.WriteWorkbook(stream,
new Sheet<Order>("Orders", orders),
new Sheet<Item>("Items", items));
读取
// 同步
var rows = Xlsx.Read<Order>(stream).ToList();
// 异步
await foreach (var o in Xlsx.ReadAsync<Order>(stream))
Process(o);
属性标注
public class Order
{
[ExporterHeader(Name = "订单号", Width = 30)]
public string OrderNo { get; set; }
[DisplayFormat(DataFormatString = "0.00")]
public decimal Amount { get; set; }
[ExporterHeader(IsIgnore = true)]
public DateTime CreatedAt { get; set; }
}
标注优先级:fluent .WithName() > [ExporterHeader] > [Display(Name=)] > [Description] > 属性名。
模板导出
基于 .xlsx 模板,把单元格里的 {{属性名}} 占位符替换为数据值;{{#集合}}…{{/集合}} 列表块逐行展开:
await Xlsx.ExportByTemplateAsync("template.xlsx", "output.xlsx", data);
模板原有的样式、合并单元格、图片、公式全部保留。行号、公式引用自动平移。
与其他 Excel 库的对比
.NET 生态中主流的 Excel 库各有各的侧重点。我们在 BenchmarkDotNet 中跑了 Magicodes.IE.IO、EPPlus、MiniExcel、ClosedXML、OpenXML SDK 的横向对比。
参与者简介
| 库 | 模式 | 依赖 | 特点 |
|---|---|---|---|
| Magicodes.IE.IO | 流式 | 无(net6+ 纯 BCL) | 自研 ZIP + CRC + OOXML,边算边写 |
| MiniExcel | 流式 | 无 | 轻量,性能出色,功能克制 |
| EPPlus (v5+) | DOM | ImageSharp 等 | 功能完整,商业授权(v5+) |
| ClosedXML | DOM | 多个 | API 优雅,功能全面 |
| OpenXML SDK | DOM | 无 | 微软官方,低层 API |
写入性能对比
.NET 10 / Apple M4 / 4 列字符串 × 100k 行(Fastest 压缩):
| 库 | 方法 | 耗时 | 分配 |
|---|---|---|---|
| Magicodes.IE.IO | Xlsx.ToBytes() 基准 |
30.3 ms | 8.91 MB |
| Magicodes.IE.IO | ToBytes() + 宽松引用 |
20.4 ms | 7.14 MB |
| Magicodes.IE.IO | Source Gen 快路径 | 21.1 ms | 3.60 MB |
| Magicodes.IE.IO | Write(Stream) 流式 |
~38 ms | ~72 KB |
| MiniExcel | SaveAs() |
149.2 ms | 213.69 MB |
| EPPlus | ExcelPackage.Save() |
455.3 ms | 257.31 MB |
| ClosedXML | XLWorkbook.SaveAs() |
678.7 ms | 805.99 MB |
与其他库的倍数对比:
| 对比 | 速度倍数 | 分配倍数 |
|---|---|---|
| vs MiniExcel | 4.9x 更快 | 24.0x 更小 |
| vs EPPlus | 15.0x 更快 | 28.9x 更小 |
| vs ClosedXML | 22.4x 更快 | 90.5x 更小 |
上面是
ToBytes()便利层的对比。若用流式Write(Stream),Mio 分配仅 ~72KB(固定),与其他库的差距拉到 3000x+。
基准项目完整覆盖了
1k/10k/50k/100k四种数据量、string/number/datetime/boolean/mixed/styled/SST(高重复字符串,自动去重)七个维度、Fastest/NoCompression两种压缩档位。完整数字执行:dotnet run -c Release -f net10.0 --project src/Magicodes.IE.Benchmarks -- \ --filter "*XlsxIO_Benchmarks*" --job short --memory
快速对比:各库优势与适用场景
| 维度 | Magicodes.IE.IO | MiniExcel | EPPlus | ClosedXML | OpenXML SDK |
|---|---|---|---|---|---|
| 运行时依赖 | ⭐⭐⭐ 无 | ⭐⭐⭐ 无 | ⭐ 多个 | ⭐ 多个 | ⭐⭐ 仅 BCL |
| 授权 | MIT | MIT | 商业(v5+) | MIT | MIT |
| 写入内存 | ⭐⭐⭐ 流式 ~72KB | ⭐⭐⭐ 流式 | ⭐ DOM | ⭐ DOM | ⭐ DOM |
| 写入速度 | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | ⭐⭐ | ⭐ |
| 功能完整度 | ⭐⭐ 常用 | ⭐⭐ 轻量 | ⭐⭐⭐ 全面 | ⭐⭐⭐ 全面 | ⭐⭐⭐ 底层 |
| 异步流 | ⭐⭐⭐ 原生 | ⭐ 有限 | ⭐⭐ Task | ⭐⭐ Task | ⭐⭐ Task |
| AOT/裁剪 | ⭐⭐⭐ SG | ⭐⭐ | ⭐ | ⭐ | ⭐⭐ |
| API 易用性 | ⭐⭐⭐ 单入口 | ⭐⭐⭐ 简洁 | ⭐⭐ | ⭐⭐⭐ 优雅 | ⭐ 底层 |
选型建议
- 类库/NuGet 包作者:选
Magicodes.IE.IO——零依赖,不会给下游带来任何负担 - 轻量导出 / 已有 MiniExcel:两者流式性能接近,Mio 在异步流和 AOT 上更强
- 复杂格式 / 图表 / 打印 / 高级样式:选 EPPlus 或 ClosedXML——功能最全
- 需要底层控制:选 OpenXML SDK
- Web 应用批量导出 + 异步流:
Magicodes.IE.IO的IAsyncEnumerable原生支持是它独有的优势
高级功能速览
以下功能默认通过
XlsxWriter底层 API 调用;常用场景建议先看Xlsx.Write()够不够。
数据验证(下拉列表)
using var writer = new XlsxWriter(stream, "订单表");
writer.AddDataValidation(new DataValidation("C2:C1000",
DataValidationType.List, "\"已下单,已发货,已完成\""));
公式
Xlsx.ToBytes(items, p => p
.Column(x => x.Qty, c => c.WithName("数量"))
.Column(x => x.Price, c => c.WithName("单价"))
.Column(x => x.Total, c => c.WithName("合计")
.WithFormula("A{row}*B{row}")));
{row} 自动替换为当前行号(1-based),展开为 A2*B2、A3*B3…
共享字符串表(SST)
对大量重复字符串的大文件自动启用 SST 去重:
Xlsx.ToBytes(orders, p => p.WithAutoSst(true));
预扫前 64 行,字符串去重比例低于 70% 时自动切到 SST,减少文件体积。
自动筛选、合并单元格、超链接、行过滤
var bytes = Xlsx.ToBytes(data, p => p
.Where(x => x.Amount > 0) // 只导出符合条件的行
.WithAutoFilter("A1:E1") // 自动筛选
.MergeCells("A1:B1") // 合并单元格
.AddHyperlink("A1", "https://...")); // 超链接
表格、打印设置、保护、批注、条件格式、大纲
见 tests/Magicodes.IE.IO.Tests/ 目录下的完整示例。覆盖了日常常见的 Excel 操作需求。
压缩档位
// 默认 Fastest
Xlsx.ToBytes(data);
// 纯 CPU 优先时关闭压缩
Xlsx.ToBytes(data, options: new XlsxWriteOptions
{
Compression = CompressionLevel.NoCompression
});
// 文件需要走网络传输、体积优先
Xlsx.ToBytes(data, options: new XlsxWriteOptions
{
Compression = CompressionLevel.Optimal
});
与老 Magicodes.IE.Excel 的区别
| 维度 | 老 Magicodes.IE.Excel |
新 Magicodes.IE.IO |
|---|---|---|
| 第三方依赖 | EPPlus | 无(net6+ 纯 BCL) |
| 写入模型 | DOM 全量构建 | 流式边写边序列化 |
| 读取 | EPPlus 封装 | 自研流式 Reader |
| 多 Sheet | 复杂 | WriteWorkbook(...) 一行 |
| 异步流 | 不支持 | IAsyncEnumerable<T> 原生支持 |
| AOT / 裁剪 | 不友好 | Source Generator 生成,无需反射 |
| API 入口 | 多接口多抽象 | Xlsx.Write() / Xlsx.Read() 统一静态入口 |
| 包体积 | 大 | 极小(net6+ 零额外依赖) |
AOT / NativeAOT 支持
为 DTO 加一个 [XlsxExportable] 特性,Source Generator 即生成属性 getter、cell writer、列元数据和 cell setter。读写路径完全不碰反射,适用于 NativeAOT 和剪裁场景:
[XlsxExportable]
public class Order
{
public string OrderNo { get; set; }
public decimal Amount { get; set; }
}
// 和普通用法完全一样
var bytes = Xlsx.ToBytes(orders);
普通 .NET 应用无需标注,直接反射即可——这是渐进式的 AOT 支持。
迁移指南
从老 Magicodes.IE.Excel 迁移只需三步:
- 把
services.AddExcelExporter()注册删掉 - 把旧的导出/导入入口替换为
Xlsx.Write()/Xlsx.ToBytes()/Xlsx.Read() ExporterHeaderAttribute仍然兼容;ExportDtoAttribute不再需要(直接用ExportProfile<T>的 fluent API)
总结
Magicodes.IE.IO 解决的核心问题是:不引入 Excel 库依赖的前提下,提供高性能、低分配的 xlsx 读写能力。特别适合:
- 类库 / NuGet 包作者:零依赖,下游不会因此引入任何 Excel 库
- Web 应用导出场景:流式写入响应流,内存峰值恒定
- 大数据导出:配合
IAsyncEnumerable实现数据库流式查询 → 直接写 xlsx - AOT 场景:Source Generator 消除反射,NativeAOT 友好
仓库地址:github.com/dotnetcore/Magicodes.IE
NuGet:dotnet add package Magicodes.IE.IO
如果对你有帮助,欢迎 ⭐ Star。遇到问题或功能建议,直接提 Issue。也可以加入 QQ 群 85318032 交流(添加时注明 Magicodes.IE)。
许可协议:MIT