
简介面向ASP.NET Core开发者的Excel导入导出实战资料介绍使用EPPlus.Core在Windows、Linux、Mac跨平台环境中处理xlsx文件。内容围绕导出与导入两大场景展示在控制器中创建ExcelPackage、添加工作表、写入表头与数据、保存并返回文件下载以及读取上传的Excel并遍历单元格解析数据的完整思路同时说明Linux下安装libgdiplus的注意事项对数据迁移、备份、分析等场景有直接参考价值。资料为单份PDF文档共1个文件大小43KB便于快速查阅。已有2869人学习下载。通过阅读可掌握EPPlus.Core的基本用法、文件流处理方式与常见异常处理要点减少实际开发中的踩坑成本。适合需要快速实现Excel导入导出功能、熟悉ASP.NET Core基础的中级开发者。1. 为什么「导入导出 Excel」折腾了三个晚上后端要出一张用户报表前端百般催促你搜了一圈「ASP.NET Core 导入导出Excel xlsx 文件实例」发现网上答案不是抄来抄去就是停留在 .NET Framework 时代的 Interop。真正动手才发现xlsx 不是普通表格文件机制、内存、日期、Content-Type 处处是坑。这篇文章就是把这些坑提前踩平先讲清楚 Excel 处理在 ASP.NET Core 里的选型逻辑再给出导出、导入的可复现代码最后把最容易翻车的高频事故逐个排查掉。适合被报表需求逼着上手的 .NET 从业者也适合想把现有导入导出代码从「能用」改到「稳定」的熟手。2. xlsx 的底牌与框架选型Interop 为什么出局四款处理库怎么选2.1 先拆开 xlsx 看内部结构才知道框架在做什么xlsx 本质上是一个 zip 压缩包用解压工具打开后能看到[Content_Types].xml、xl/workbook.xml、xl/worksheets/sheet1.xml、xl/styles.xml、xl/sharedStrings.xml这一整套 OOXML 结构。那层 Excel 界面只是壳真正的数据、样式、字符串表全躺在这一堆 xml 里。理解这一点很多问题就有了判断依据比如「文件格式或文件扩展名无效」多半是 Content-Type 写错或者响应里塞了非 zip 内容又比如「导出后日期变成一串数字」是因为 Excel 内部用 OADate 序列号存储日期需要样式表配合格式化。另一个必须淘汰的旧思路是 Microsoft.Office.Interop.Excel。它要求服务器装 Office且 COM 组件只在 Windows 上可用并发一高就出现「无法将类型为 COM 对象的类强制转换为接口类型」之类的玄学错误。ASP.NET Core 的部署目标普遍是 Linux 容器这条路基本走不通。官方文档也明确不建议在服务端用 Office 自动化。它作为终端用户本机操作没问题挪到 Web 后端就是定时炸弹。2.2 四款主流 Excel 处理框架的取舍我把社区里最常用的几个方案放在一起做过对比。NPOI 免费开源API 偏老但稳定读写都支持HSSFWorkbook管 xlsXSSFWorkbook管 xlsx缺点是全量驻留内存没有流式写EPPlus 4.5 以后改成了商业授权5.0 起要求设置LicenseContext否则运行时直接抛异常功能确实现代但授权合规要去研究文档ClosedXML 基于 OpenXML SDK 封装API 用 LINQ 风格上手快生成带样式的表格非常方便代价是内存占用更高ExcelDataReader 只读流式解析内存友好适合导入场景读 xls 还要搭配编码包。那导入导出是不是要装一堆包我的用法是混装各干各的导出中量级报表用 ClosedXML写代码快导入 Excel 用 ExcelDataReader内存可控遇到十几万行的大导出退回 NPOI 配合临时文件方案。表格式的对比结论如下。框架读写授权内存表现适用场景NPOI读写免费宽松全量驻留高兼容性兜底大导出配临时文件EPPlus读写5.0 起商业授权全量驻留中商业项目且有授权预算ClosedXML读写MIT偏高中小量导出样式复杂报表ExcelDataReader只读宽松流式低导入解析单表几十万行我再补充一个选型建议如果你的项目同时面对旧系统传上来的 xls 和新的 xlsx首选 NPOI 兜底不要赌所有客户端都会转格式。ExcelDataReader 读 xls 需要额外引入ExcelDataReader.Encoding包否则字符串会乱码。这个后面会提到。3. 导出 xlsxClosedXML 最小实现与大数据量下的换路3.1 用 ClosedXML 在 ASP.NET Core 跑通第一版导出下面这段是我在 Web API 里导出用户表的完整代码重点不是多复杂而是把「内存流 → FileResult → 浏览器下载」这个链路走通。[HttpGet(export/users)] public IActionResult ExportUsers() { // 数据从数据库查出这里只做演示 var users new ListUserDto { new UserDto { Name 张三, CreatedAt DateTime.Now } }; using var workbook new XLWorkbook(); var ws workbook.AddWorksheet(用户列表); // 表头与样式 ws.Cell(1, 1).Value 姓名; ws.Cell(1, 2).Value 创建时间; ws.Range(1, 1, 1, 2).Style.Font.Bold true; ws.Range(1, 1, 1, 2).Style.Fill.BackgroundColor XLColor.LightGray; // 数据行第 2 行开始写 for (int row 0; row users.Count; row) { ws.Cell(row 2, 1).Value users[row].Name; ws.Cell(row 2, 2).Value users[row].CreatedAt; ws.Cell(row 2, 2).Style.DateFormat.Format yyyy-MM-dd HH:mm:ss; } ws.Columns().AdjustToContents(); // 关键不要用 using 包住 MemoryStream响应发送前释放会翻车 var ms new MemoryStream(); workbook.SaveAs(ms); ms.Position 0; return File(ms, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, $用户列表_{DateTime.Now:yyyyMMddHHmmss}.xlsx); }这里有个新手最容易栽的细节MemoryStream不要用using包裹。File()返回的是FileStreamResult框架在响应发送完毕后才会释放这个流如果在 Action 里提前 using流在写出前就被 Dispose浏览器下载到的就是 0 字节或损坏文件。另一个必填项是 Content-Typexlsx 要写死application/vnd.openxmlformats-officedocument.spreadsheetml.sheet写成application/octet-stream也可以下载但某些安全软件或代理会拦截Excel 也可能提示格式和扩展名不匹配。3.2 导出必调的 5 个参数与 10 万行数据怎么办第一版跑通后接下来要按业务需求调参数。我一般固定调这几个日期格式用DateFormat.Format统一成yyyy-MM-dd HH:mm:ss否则 Excel 可能显示成 12 小时制列宽用AdjustToContents()自动适应但列数超过 50 列时逐列自适应比较慢改成对关键列单独Width 18冻结首行用ws.SheetView.FreezeRows(1)方便用户下拉看数据时始终看到表头大文本列要主动设置换行Style.Alignment.WrapText true不然长备注会被截断最后数字列别用Value塞字符串会让求和变文本。当数据量到了 10 万行以上ClosedXML 的内存占用会明显飙升因为它把所有单元格对象都构建在内存里。我实测过 20 万行带样式的导出进程内存能涨到 1GB 以上服务器小一点直接黑匣子式崩溃。这时候我一般换 NPOI并且绕开全驻留思路——先写到服务器临时文件再用FileStream返回给客户端。[HttpGet(export/users-large)] public IActionResult ExportUsersLarge() { var tmpFile Path.GetTempFileName(); // 生成一个临时物理文件 try { using (var fs new FileStream(tmpFile, FileMode.Create)) using (var workbook new XSSFWorkbook()) // XSSF 管 xlsx { var sheet workbook.CreateSheet(用户列表); var header sheet.CreateRow(0); header.CreateCell(0).SetCellValue(姓名); header.CreateCell(1).SetCellValue(创建时间); int rowIndex 1; foreach (var user in GetUsersPaged()) // 分页从数据库读避免一次全量 { var row sheet.CreateRow(rowIndex); row.CreateCell(0).SetCellValue(user.Name); row.CreateCell(1).SetCellValue(user.CreatedAt); rowIndex; } workbook.Write(fs); // 直接写磁盘不走内存流 } var downloadStream new FileStream(tmpFile, FileMode.Open, FileAccess.Read); return File(downloadStream, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, $用户列表_{DateTime.Now:yyyyMMddHHmmss}.xlsx); } finally { // 这里不要立即删除 tmpFile响应可能还没发送完 Response.OnCompleted(() { System.IO.File.Delete(tmpFile); return Task.CompletedTask; }); } }这段代码的关键在于XSSFWorkbook对应的是 xlsx不要用HSSFWorkbook否则扩展名与文件实际格式不符临时文件用Response.OnCompleted延迟删除这是下载完成后的后悔药切入点。至于为什么不用SXSSFWorkbook式的流式写——NPOI 在 .NET 侧没有提供对应的 SXSSF API想要流式导出只能自己分页生成多个 sheet或者直接上 OpenXML SDK 手写 xml那样开发成本很高一般业务用不上。4. 导入 xlsx上传校验、流式读取与批量入库的完整链路4.1 上传接口扩展名白名单加文件头校验别信文件名导入和导出是独立的一条链路。很多人只检查扩展名但扩展名是用户可以随便改的。把 .txt 改成 .xlsx 上传后端若不做内容校验解析时就会抛出各种奇怪异常。这里我用「扩展名 文件头」双重校验xlsx 是 zip 包文件头固定为PK0x50 0x4Bxls 则是 OLE 复合文档头两个字节是D0 CF。[HttpPost(import/users)] [RequestSizeLimit(10_000_000)] // 限制 10MB防大文件拖垮进程 public async TaskIActionResult ImportUsers(IFormFile file) { if (file null || file.Length 0) return BadRequest(文件为空); var ext Path.GetExtension(file.FileName).ToLowerInvariant(); if (ext ! .xlsx ext ! .xls) return BadRequest(仅支持 .xlsx / .xls 文件); using var input file.OpenReadStream(); // 读前 4 字节判断真实文件类型 var header new byte[4]; await input.ReadAsync(header); bool isZip header[0] 0x50 header[1] 0x4B; // PK bool isOle header[0] 0xD0 header[1] 0xCF; // 老版 xls if (!isZip !isOle) return BadRequest(文件内容不是有效的 Excel 文件); input.Position 0; // 校验完记得复位交给解析器重新读取 // 交给下一层解析与入库 return Ok(await ImportUsersFromExcel(input)); }校验逻辑顺手说明RequestSizeLimit限制的是请求体大小防的是有人传一个 2GB 的「Excel」进来input.ReadAsync读完 4 字节后必须把Position复位到 0不然解析器会从第 5 字节开始读肯定乱套OLE 格式判断应对的正是老系统还在传.xls的场景。4.2 ExcelDataReader 流式解析与 SqlBulkCopy 入库解析步骤我优先选 ExcelDataReader因为它是流式读不会把整个工作簿一次性加载进内存。下面这段把第一张 sheet 读成DataTable再用SqlBulkCopy批量入库。using System.Text; private async Taskint ImportUsersFromExcel(Stream stream) { Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); // 兼容 xls 的 GBK 编码 using var reader ExcelReaderFactory.CreateReader(stream); var ds reader.AsDataSet(new ExcelDataSetConfiguration { ConfigureDataTable _ new ExcelDataTableConfiguration { UseHeaderRow true, // 第一行作为列名 ReadHeaderRow rowReader { /* 可以在读表头时做列名重命名 */ } } }); var dt ds.Tables[0]; if (dt.Rows.Count 0) return 0; using var bulk new SqlBulkCopy(_connectionString) { DestinationTableName Users, BatchSize 5000 }; // 按列名映射避免 Excel 列顺序与表结构不一致 foreach (DataColumn col in dt.Columns) bulk.ColumnMappings.Add(col.ColumnName, col.ColumnName); await bulk.WriteToServerAsync(dt); return dt.Rows.Count; }这段的重点UseHeaderRow true表示第一行当列名但要求 Excel 表头必须和数据库列名完全一致否则映射失败BatchSize 5000是每批次写入的行数可以根据服务器性能调太小慢太大占内存SqlBulkCopy走的是批量插入比循环ExecuteNonQuery快一到两个数量级。需要提醒的是AsDataSet在数据量超过 30 万行时会一次性构出整个 DataTable内存仍会涨。真正的极限导入应该直接while (reader.Read())逐行处理合并成自定义 DTO 再分批入库代价是代码要多写不少。5. xlsx 实战高频避坑文件损坏、日期串号、格式错乱怎么排查5.1 下载后提示「文件格式或文件扩展名无效」现象接口返回 200文件也能下载但双击打开Excel 报格式或扩展名无效。 原因最常见的是 Content-Type 写成了text/html或application/octet-stream浏览器按 MIME 处理时把二进制内容当成文本下载另一种是文件名后缀和实际文件格式不匹配比如用HSSFWorkbook生成 xls 逻辑却命名成 .xlsx。 解决把 Content-Type 固定为application/vnd.openxmlformats-officedocument.spreadsheetml.sheet确认 NPOI 的XSSFWorkbook对应 xlsx、HSSFWorkbook对应 xls两者不能互串如果接口前面还挂了 Nginx 等代理要检查代理有没有改写响应头。5.2 Excel 里日期变成 44123 一串数字现象导出文件中本该显示2024-01-15 10:30:00实际却显示为45406之类的数字或者导入时日期列读出来是 double 类型。 原因Excel 内部不存字符串日期而是存 OADate 序列号。导出的单元格类型是数字没配日期格式导入时GetValue()拿到的是double没有转成 DateTime。 解决导出时给日期单元格写真正的时间对象并设置Style.DateFormat.Format导入时判断列类型reader.GetFieldType(index) typeof(double)就对值调用DateTime.FromOADate(value)转回时间。这是这个方向里最典型的「黑匣子」问题不查源码很难想到。5.3 MemoryStream 被提前释放文件间歇性损坏现象本地调试一切正常部署到 Linux 服务器后文件时好时坏有时 0 字节有时打开提示文档损坏。 原因Action 里用using var ms new MemoryStream()函数返回前流已经被 DisposeFileStreamResult还没开始发送数据。本地 CPU 快、时序靠运气能蒙混过关服务器一忙就暴露。 解决把using去掉让return File(ms, contentType, fileName)的FileStreamResult接管流的生命周期如果你手动new FileStreamResult也注意不要在返回前关流。5.4 xlsx 变成 xlsm扩展名与文件内容对不上现象导出后的文件后缀是 .xlsx但 WPS 或 Excel 提示文件被改成了 xlsm或者反过来上传 xlsx 后解析报错。 原因xlsm 是带宏的工作簿内部[Content_Types].xml里声明的是macroEnabledMain。有同事在写 NPOI 时用XSSFWorkbook构造了带宏的类型保存出来实际就是 xlsm更多人则是下载时把文件名写成了xxx.xlsm.xlsx。 解决导出前明确XSSFWorkbook默认不带宏保存的Content-Type与扩展名一一对应xlsx 就用 spreadsheetml.sheetxlsm 要用application/vnd.ms-excel.sheet.macroEnabled.12。不要让文件名里出现.xlsx.xlsm这种拼接结果。5.5 客户端文件被占用导入读取一直失败现象用户开着同一个 Excel 文件点导入时报「文件正被另一进程使用」但在 Web 场景下服务器读的是上传副本理论上不该被占用。 原因常见于用户上传的是他自己网盘同步目录里的文件或者浏览器插件、杀毒软件对临时目录加锁另一种是File.OpenReadStream()之后没有 Dispose前一次请求的文件句柄没释放下一次导入撞上锁。 解决IFormFile的流一定放在using里不要在解析完成前把流交给异步任务若确认是客户端本地占用直接提示用户先关闭相关程序再操作这是在服务端无法用代码绕开的现实。6. 用 OpenXML SDK 给每个 xlsx 上最后一道保险当导出的 Excel 频率高、样式复杂时肉眼打开看一遍并不够尤其是发版前改动了表头或日期格式很多细碎问题要到用户手里才爆出来。我现在的习惯是写一个自动化校验在集成测试里对导出的 xlsx 做结构级检查比人工「打开看一眼」可靠得多。// 安装包DocumentFormat.OpenXml using DocumentFormat.OpenXml.Packaging; public void ValidateXlsx(byte[] bytes) { using var ms new MemoryStream(bytes); using var doc SpreadsheetDocument.Open(ms, false); // 只读打开 var workbookPart doc.WorkbookPart; if (workbookPart null) throw new Exception(缺少 workbook 部件文件不是有效 xlsx); var sheetCount workbookPart.WorksheetParts.Count(); if (sheetCount ! 1) throw new Exception($预期 1 个 sheet实际 {sheetCount} 个); // 检查表头关键列是否存在 var sheetData workbookPart.WorksheetParts.First().Worksheet .DescendantsCell().FirstOrDefault(c c.InnerText 姓名); if (sheetData null) throw new Exception(表头缺少「姓名」列); }这段测试放在 CI 里每次导出逻辑改动后自动跑一遍很多问题都能提前拦住。我把这套校验理解成一种「后置的真实感验证」——代码写完之后不是看它没报错就算完而是让程序自己确认产出的二进制真的能被 OpenXML SDK 认出来。这也是我唯一能给出的、应对各类疑难杂症的确定性手段。回到最初那个场景与其被前端催着上线一份时好时坏的导入导出不如第一次就按「选型 → 导出 → 导入 → 避坑 → 校验」这条路走完。尤其是日期和 Content-Type 这两个坑我在这个方向上栽过不下三次后来凡是动过导出代码都先跑一遍上面的校验再发版基本杜绝了「我这边没问题啊」的尴尬。希望帮到你。本文还有配套的精品资源点击获取