在C#生态中处理Excel文件,很多团队习惯用Epplus或NPOI,但遇到几十万行数据时就容易内存爆满。MiniExcel是一个基于流式读写设计的开源库,它不把整个文档加载到内存,而是按行处理,因此特别适合导入导出大体积表格。下面直接看具体用法。

一、环境准备与安装
MiniExcel以NuGet包形式发布,支持.NET Core 3.1及以上以及.NET Framework 4.6.1以上版本。在项目中引入非常简单,可以通过命令行或者Visual Studio的包管理器完成。安装后无需任何复杂配置,直接using对应命名空间即可使用。
为了验证环境,我们可以先建立一个控制台项目,然后执行安装命令。需要注意的是,MiniExcel依赖System.IO.Packaging,在部分旧框架中会自动 transitively 引用,一般不用手动处理。如果你在Linux下使用,确保运行时具备写临时文件的权限,因为底层会借助临时流提升性能。
// 通过NuGet安装
// Install-Package MiniExcel -Version 1.31.0
using MiniExcelLibs;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
Console.WriteLine("MiniExcel ready");
}
}
二、基础写入Excel
MiniExcel最基础的写法是把集合直接保存为xlsx文件。它支持匿名对象、强类型类以及Dictionary等多种数据源。写入时默认首行为属性名或字典键,后续每行对应一条记录。由于采用流式写出,即使十万条数据也不会出现明显内存增长。
下面示例定义一个订单类,并生成包含三列的文件。相比传统组件先建Workbook再建Sheet,MiniExcel一行静态方法就完成。你也可以通过第二个参数指定sheet名称,或者传入Stream实现网络响应写出。这种极简API显著降低了代码量,也减少了出错概率。
public class Order
{
public int Id { get; set; }
public string Product { get; set; }
public decimal Price { get; set; }
}
var orders = new List<Order>
{
new Order { Id = 1, Product = "键盘", Price = 99.9m },
new Order { Id = 2, Product = "鼠标", Price = 59.5m }
};
// 同步写入本地文件
MiniExcel.SaveAs("orders.xlsx", orders, sheetName: "订单");
// 异步写入
await MiniExcel.SaveAsAsync("orders_async.xlsx", orders);
三、读取Excel数据
读取同样保持流式风格。使用MiniExcel.Query方法可延迟加载每一行,返回IEnumerable,配合强类型映射时自动按头行匹配属性。这样在导入大文件时,可以边读边做业务校验,而不必等全部载入。
如果Excel首行不是标准标题,可以通过UseHeaderRow参数控制。对于脏数据较多的场景,建议用动态类型读取成Dictionary再逐字段处理。以下代码展示如何把刚才生成的文件读回,并输出到控制台。注意读取时不支持随机跳转,这是为了保证低内存而做的设计权衡。
// 强类型读取
var rows = MiniExcel.Query<Order>("orders.xlsx");
foreach (var r in rows)
{
Console.WriteLine($"{r.Id} {r.Product} {r.Price}");
}
// 动态读取为字典
var dynamicRows = MiniExcel.Query("orders.xlsx", useHeaderRow: true);
foreach (var d in dynamicRows)
{
// d 是 IDictionary<string, object>
Console.WriteLine(d["Product"]);
}
四、模板填充实战
报表常要求固定格式,比如表头带公司Logo、合并单元格等。MiniExcel支持基于已有模板的填充,只需在Excel里用@字段名做占位符,代码传入对象即可替换。它比手工画表稳定,也方便非开发人员维护模板。
假设有模板文件template.xlsx,其中某个单元格写了@UserName和@Total,我们传入匿名对象完成导出。模板机制同样走流,不会把整文件读进内存。下面的例子演示单对象填充,它也支持集合填充配合@List语法实现循环表格,这里先给出基础用法。
var fillData = new
{
UserName = "张三",
Total = 159.4m
};
MiniExcel.SaveAsByTemplate("report.xlsx", "template.xlsx", fillData);
五、Web场景导入导出
在ASP.NET Core里,MiniExcel能直接对接HttpResponse.Body流,避免中间文件。导出时设置Content-Type为application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,并给Content-Disposition附文件名。导入则读取IFormFile.OpenReadStream,交给Query处理。
这种写法在并发导出时优势明显,因为每个请求只占少量托管内存。下面的控制器示例展示了下载与上传两个接口。上传部分用await MiniExcel.QueryAsync逐行入库,你可以插入数据库或做校验。注意大文件上传要在Startup限制表单大小,否则会被拒绝。
using Microsoft.AspNetCore.Mvc;
using MiniExcelLibs;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
[ApiController]
[Route("api/excel")]
public class ExcelController : ControllerBase
{
[HttpGet("download")]
public async Task<IActionResult> Download()
{
var data = new List<Order> { new Order { Id = 1, Product = "测试", Price = 10 } };
Response.Headers.Add("Content-Disposition", "attachment; filename=test.xlsx");
Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
await MiniExcel.SaveAsAsync(Response.Body, data);
return new EmptyResult();
}
[HttpPost("upload")]
public async Task<IActionResult> Upload(IFormFile file)
{
var rows = await MiniExcel.QueryAsync<Order>(file.OpenReadStream(), useHeaderRow: true);
foreach (var item in rows)
{
// 此处可写入数据库
}
return Ok();
}
}
六、性能与注意事项
从压测看,导出一百万行乘十列,MiniExcel内存常驻在30MB左右,而NPOI可能突破800MB。代价是它不支持复杂样式和图表,若需求只是数据交换,这完全可接受。另外由于是流式,不能修改已写出的行,只能追加。
使用中应避开在读取时多次枚举同一Query结果,因为每次都会重新解析流。建议用ToList缓存或者一次性处理。对于包含公式的单元格,MiniExcel默认读取值而非公式,如需公式可用GetReader底层API。掌握这些边界,就能在C#项目里把Excel处理变得轻松且健壮。
| 对比项 | MiniExcel | 传统组件 |
|---|---|---|
| 内存占用 | 低,流式 | 高,全加载 |
| 大文件支持 | 优 | 弱 |
| 样式能力 | 基础 | 丰富 |