最省事方案是用EPPlus 6.2+(非EPPlus.Core),需设LicenseContext,DataTable列名须为合法XML名(可重命名或传自定义header),DBNull要预处理,大数据量应分页或换ClosedXML/CSV,导出时注意响应头和流操作完整性。

用 EPPlus 写入 DataTable 到 Excel 最省事
直接上结论:别手写 Open XML,也别用老旧的 Microsoft.Office.Interop.Excel(需要装 Office、线程不安全、容易卡死)。现在主流且稳定的选择是 EPPlus —— 它纯托管、跨平台(.NET 5+)、性能好,而且对 DataTable 有原生支持。
常见错误现象:EPPlus 6.x 起默认禁用旧式加密和某些兼容模式,如果代码里还沿用 ExcelPackage.LicenseContext = LicenseContext.NonCommercial 却没加 NuGet 许可包,会抛 System.InvalidOperationException: Set the license context;另外,.NET Core/6+ 项目若没关掉“确定性生成”,可能在 CI 环境报找不到 EPPlus 程序集。
- 安装时务必用
NuGet引入EPPlus6.2+(注意不是EPPlus.Core,那个已废弃) - 首次使用前必须设置许可上下文:
ExcelPackage.LicenseContext = LicenseContext.NonCommercial(开发/个人用)或Commericial(商用需购授权) -
LoadFromDataTable()默认跳过空行、自动推断列类型,但不会处理DBNull.Value—— 需提前把DataTable里的DBNull替换为null或空字符串,否则写入时报Object reference not set
DataTable 列名含中文或特殊字符时怎么避免报错
EPPlus 写入时会把 DataTable 的 ColumnName 当作 Excel 表头,但它内部用 XmlConvert.VerifyName() 校验列名是否合法 XML 元素名。中文、空格、点号(.)、括号等都会触发 ArgumentException: The string argument 'name' must be a valid XML name。
- 最稳妥做法:导出前重命名
DataTable.Columns,只保留字母、数字、下划线,例如dt.Columns["用户姓名"].ColumnName = "UserName" - 不想改原始列名?可以用
LoadFromDataTable(dt, printHeaders: true)的重载,传入自定义 header 数组:new[] { "用户名", "注册时间", "状态" } - 注意:列名重复也会崩,
EPPlus不做去重,得自己检查dt.Columns.Cast<datacolumn>().Select(c => c.ColumnName).Distinct().Count() == dt.Columns.Count</datacolumn>
导出大表(10 万行以上)卡顿或内存爆掉怎么办
EPPlus 默认把整个工作簿加载进内存,写入 10 万行 + 多列的 DataTable 时,很容易吃光几百 MB 内存,甚至触发 GC 停顿导致 UI 假死。这不是 bug,是设计使然 —— 它没流式写入 API。
- 能分页就分页:用
DataTable.AsEnumerable().Skip().Take()拆成每 5000 行一个 sheet,别硬塞一张表 - 真要单表超大?换
ClosedXML(API 类似,但底层更省内存)或直接用System.Text.Json+CSV(Excel 能直接打开 CSV,且无内存压力) - 别信“调小
ExcelPackage.Streaming就能流式写”——这个属性在 6.x 已移除,相关文档是过时的
导出后 Excel 打开提示“发现不可读取的内容”
这通常不是代码问题,而是文件落地时被破坏了:比如用 Response.OutputStream 直接写但没设对 ContentType 和 Content-Disposition,或者用 FileStream 写完没 Flush() / Close(),又或者反病毒软件劫持了文件句柄。
- Web 场景下,务必写全响应头:
Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",文件名带.xlsx后缀,且用Response.Headers.Add("Content-Disposition", "attachment; filename=report.xlsx") - 本地保存时,用
using (var p = new ExcelPackage()) { ... p.SaveAs(new FileInfo(path)); },别手动File.WriteAllBytes包裹package.GetAsByteArray()—— 后者容易丢流尾部校验数据 - 如果用了
MemoryStream中转,确认构造时传true允许重写:new MemoryStream(capacity, true),否则SaveAs()可能写不全
复杂点在于:Excel 的修复提示往往不指明哪一行哪一列出问题,它只说“部分内容丢失”。这时候得回退一步,先用最小 DataTable(2 行 2 列)验证流程通不通,再逐步加字段,重点盯 DateTime、Decimal、含换行符的字符串这三类值 —— 它们最容易在格式推断中翻车。











