Magicodes.IE.IO:零依赖、流式低分配的 .NET Excel 读写库

目录


为什么又造一个 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.0net6.0 及以上只依赖 BCL。


核心设计哲学

1. 零运行时依赖

不依赖 EPPlus、不依赖 ClosedXML、不依赖任何第三方 Excel 库。net6.0+ 目标只引用 BCLnetstandard2.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].xmlworkbook.xmlsheet1.xmlstyles.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.IOIAsyncEnumerable 原生支持是它独有的优势

高级功能速览

以下功能默认通过 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*B2A3*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 迁移只需三步:

  1. services.AddExcelExporter() 注册删掉
  2. 把旧的导出/导入入口替换为 Xlsx.Write() / Xlsx.ToBytes() / Xlsx.Read()
  3. 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