拓冰建站拓冰建站
首页 / 资讯中心 / 正文

ASP.NET Core中Excel导入导出实战:EPPlus与ExcelDataReader踩坑总结

简介面向ASP.NET Core开发者的Excel处理示例资源包以WebAPI项目中用EPPlus导出和用ExcelDataReader读取为核心解决不安装Office也能处理Excel的常见需求。资源附带一个可运行的API项目骨架包含C#源码、sln与csproj工程文件、NuGet依赖包以及JSON配置代码中演示了创建ExcelPackage、填充工作表单元格并通过MemoryStream和响应头设置返回xlsx文件同时展示使用FileStream与ExcelReaderFactory创建读取器逐行遍历工作表数据其流式处理思路可避免大文件载入内存并兼容旧版BIFF8与OpenXML格式。包内共208个文件以dll、cs、json为主另有pdb调试文件、exe及跨平台运行库等辅助内容压缩包体积约21.89MB工程结构清晰便于在Visual Studio中直接编译运行或迁移到企业级文件上传、数据导出模块中复用。已有399人学习下载适合需要快速掌握ASP.NET Core环境下Excel读写技能的中高级开发者。 后台管理系统里十个需求有八个都绕不开 Excel上传一个表格把数据读进库或者从库里拉数据导成 Excel 给业务方。我之前把这两个功能写在一个 excel-handler 服务里技术栈锁定 ASP.Net Core WebApi EPPlus ExcelDataReader跑了一年多踩了不少坑也沉淀出一套比较稳的写法。这篇就把它拆开讲清楚适合正在做 .NET 后台、被 Excel 导入导出折磨过的同学参考。你会看到我为什么把读和写拆成两个库以及实际编码时那些文档里查不到的细节。1. excel-handler整体思路导入导出为什么不用同一个库1.1 导入和导出本质是两个相反的诉求刚开始做这个功能时我也想过“一把梭”既然 EPPlus 既能读又能写干脆全用它。结果做完导入模块我就后悔了。读 Excel 的核心诉求是“把文件里的单元格快速变成内存里的结构化数据”要的是解析速度快、容错能力强、能处理乱七八糟的表格写 Excel 的核心诉求是“把结构化数据变成用户体验友好的文件”要的是样式控制、公式、下拉框、筛选器、打印设置这些能力。这两个方向是相反的。让一个库同时做到极致要么体积庞大要么在某个方向上的体验很别扭。EPPlus 的读取能力不弱但它更擅长的是“创建”和“操作”一个 Excel 文件模型而 ExcelDataReader 则是专职的流式读取器轻量、无重依赖、读取性能好。把“读”和“写”分别交给最合适的库代码职责清楚后续维护也不用在一个类里翻来翻去。1.2 EPPlus 与 ExcelDataReader 的分工实际项目里我定了一条规则所有“导出”“模板下载”“报表生成”走 EPPlus所有“上传解析”“数据导入”走 ExcelDataReader。两者的边界用接口隔开我在 service 层暴露的是IExcelImportService和IExcelExportServiceController 不关心底层到底用了哪个库。能力EPPlusExcelDataReader写入 xlsx支持核心强项不支持读取 xlsx支持但体验一般支持流式轻快读取 xls 老格式不支持支持样式 / 公式 / 图表 / 数据验证支持不支持内存表现大文件导出耗内存需控制方式流式读取内存友好开源与授权5 版本有商用授权限制MIT 免费这表格我常发给团队新同学看。EPPlus 负责“生产 Excel”ExcelDataReader 负责“消费 Excel”各管一段互补而不重叠。除了功能边界还有一个隐性原因解耦。以后如果 ExcelDataReader 不够用了想换成 MiniExcel 或者自己解析 OpenXML只动 import 服务内部实现就行导出完全不受影响。2. 选型之前必须搞懂的三个底层细节2.1 EPPlus 的 License 陷阱别等上线才踩到这一点我必须放在最前面说因为它直接决定你能否在商业项目里用。EPPlus 在 4 版本之前是 LGPL 协议5 版本之后改成了 PolyForm Noncommercial 1.0.0非商业场景免费商业场景需要购买商业授权。很多人从老项目迁到新版编译没问题一运行直接抛异常。异常信息大致会提示你需要设置ExcelPackage.LicenseContext。解决方式是在程序启动时加一行using OfficeOpenXml; ExcelPackage.LicenseContext LicenseContext.NonCommercial;加在Program.cs的var app builder.Build();之前即可。注意这不是写在某个 Controller 里而是进程级静态配置整个进程只需要设置一次。如果你们公司是商业项目请一定去 EPPlus 官网确认授权规则该买许可证就买如果不想为这个付费就要趁早评估 ClosedXML 或 NPOI 作为备选。我在小项目里见过有人无视授权直接用这是有合规风险的隐患不建议。2.2 ExcelDataReader 的“只读”哲学ExcelDataReader 最值钱的设计是流式读取。它不会像一些组件那样把整个 Excel 文件模型加载进内存而是从头到尾按顺序扫单元格既快又省内存。这个特性在导入几十 MB 的大文件时优势特别明显。使用上有两个容易忽略的点。第一要读.xls老格式需要额外注册代码页编码using System.Text; Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);不加这行读.xls文件里带中文时很容易乱码。这是我最早踩过的坑当时排查了好久最后发现就是少了这一句。第二AsDataSet扩展方法在独立的ExcelDataReader.DataSet包里只装了主包是找不到这个方法的需要显式引包。2.3 流式处理与内存边界很多人写 Excel 接口时习惯把文件先读进MemoryStream再传给解析库。小文件无所谓文件一大内存直接报警。上传场景里IFormFile.OpenReadStream()本身就是可读流直接把它传给 ExcelDataReader 就行不需要中间多复制一份。导出场景同理。EPPlus 的GetAsByteArray()方法适合中小文件如果是几万行甚至几十万行的数据建议用临时文件承接输出流避免在内存里构建完整文件镜像。这个“流式处理”的思路不仅让接口更稳也是你和其他人写的代码拉开差距的地方。3. 实操记录核心接口从0到13.1 初始化项目与安装依赖我用的是 .NET 8 的 WebAPI 模板先建项目再补包dotnet new webapi -n ExcelHandler cd ExcelHandler dotnet add package EPPlus dotnet add package ExcelDataReader dotnet add package ExcelDataReader.DataSet dotnet add package System.Text.Encoding.CodePages四个包的作用分别是EPPlus 负责导出和模板生成ExcelDataReader 负责读取解析DataSet 包提供AsDataSet扩展让你把 Excel 直接转成DataTableEncoding.CodePages 是为兼容.xls老格式的中文编码。项目结构我习惯这样放Controllers/ ExcelController.cs Services/ IExcelImportService.cs ExcelImportService.cs IExcelExportService.cs ExcelExportService.cs Models/ ExportRequest.csController 只做参数绑定和结果返回业务逻辑全在 service 层后面加单元测试也方便。3.2 读取接口上传 Excel返回结构化数据先定义 Controller 的入口。前端用 FormData 上传文件后端用IFormFile接收[ApiController] [Route(api/excel)] public class ExcelController : ControllerBase { private readonly IExcelImportService _importService; public ExcelController(IExcelImportService importService) { _importService importService; } [HttpPost(read)] public async TaskIActionResult Read([FromForm] IFormFile file) { if (file null || file.Length 0) return BadRequest(new { success false, message 请上传有效的 Excel 文件 }); using var stream file.OpenReadStream(); var rows await _importService.ReadAsync(stream, file.FileName); return Ok(new { success true, count rows.Count, rows }); } }service 里的核心逻辑是创建 reader、配置表头、逐行转成字典public class ExcelImportService : IExcelImportService { static ExcelImportService() { Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); } public async TaskListDictionarystring, object ReadAsync(Stream stream, string fileName) { var ext Path.GetExtension(fileName).ToLowerInvariant(); if (ext ! .xlsx ext ! .xls) throw new NotSupportedException(仅支持 .xlsx / .xls 文件); using var reader ExcelReaderFactory.CreateReader(stream); var config new ExcelDataSetConfiguration { UseColumnDataType true, ConfigureDataTable _ new ExcelDataTableConfiguration { UseHeaderRow true } }; var dataSet reader.AsDataSet(config); var sheet dataSet.Tables[0]; var columns sheet.Columns .CastSystem.Data.DataColumn() .Select(c c.ColumnName) .ToList(); var result new ListDictionarystring, object(); foreach (System.Data.DataRow row in sheet.Rows) { var dict new Dictionarystring, object(); foreach (var column in columns) { var value row[column]; dict[column] value DBNull.Value ? null : value; } result.Add(dict); } return await Task.FromResult(result); } }两个细节值得注意。UseHeaderRow true表示把第一行当作列名这样返回的 JSON 字段就是表头文字而不是 A、B、C 这种无意义坐标UseColumnDataType true会尽量保留 Excel 单元格的真实类型日期列出来是DateTime而不是一串数字。表头这地方容易踩坑如果第一行有空列或多余空格列的ColumnName会变成F1、F2这类默认名。所以上传模板一定要在业务层做列名校验把“列名缺失”的提示抛给前端。3.3 导出接口把业务数据写成带样式的 Excel读取讲完接着是导出的核心代码。前端传一个 JSON 数组进来后端把它渲染成带表头样式、筛选、冻结行的工作表。[HttpPost(export)] public IActionResult Export([FromBody] ExportRequest request) { var bytes _exportService.BuildExcel(request.Rows, request.SheetName); return File(bytes, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, request.FileName); }ExportRequest主要是三个字段Rows是ListDictionarystring, objectSheetName是工作表名FileName是下载文件名。service 里的实现public byte[] BuildExcel(IListDictionarystring, object rows, string sheetName Sheet1) { using var package new ExcelPackage(); var ws package.Workbook.Worksheets.Add(sheetName); if (rows null || rows.Count 0) { ws.Cells[1, 1].Value 暂无数据; return package.GetAsByteArray(); } var columns rows.SelectMany(r r.Keys).Distinct().ToList(); for (var i 0; i columns.Count; i) { var cell ws.Cells[1, i 1]; cell.Value columns[i]; cell.Style.Font.Bold true; cell.Style.Fill.PatternType ExcelFillStyle.Solid; cell.Style.Fill.BackgroundColor.SetColor(Color.FromArgb(217, 225, 242)); cell.Style.HorizontalAlignment ExcelHorizontalAlignment.Center; } for (var r 0; r rows.Count; r) { var row rows[r]; for (var c 0; c columns.Count; c) { var value row.ContainsKey(columns[c]) ? row[columns[c]] : null; ws.Cells[r 2, c 1].Value SanitizeCell(value); } } ws.Cells[1, 1, rows.Count 1, columns.Count].AutoFilter true; ws.View.FreezePanes(2, 1); return package.GetAsByteArray(); }其中SanitizeCell是防止公式注入的下一节专门讲。样式部分我加了粗体表头、填充背景色、居中对齐这些在 EPPlus 里都是几行代码的事。最后两行AutoFilter和FreezePanes是业务上特别常用的功能一个给表格加筛选下拉一个冻结首行用户拿到文件后体验会好很多。这里有个性能提示AutoFitColumns()不要在大文件上直接调它会对每一列做宽度自动测量几千行没问题几万行就能明显感觉到卡。真要调列宽手动算一个最大字符串长度然后按比例设置效果接近且快得多。模板下载接口也可以用 EPPlus 加数据验证比如给某列做成下拉选“是/否”ws.DataValidations.AddListValidation(A2:A100)就能做到这是 ExcelDataReader 做不了的事。3.4 上传限制与异常兜底接口上线前一定要把文件大小限制和异常处理想清楚。Kestrel 默认的请求体上限大约是 30 MBFormOptions.MultipartBodyLengthLimit默认大约是 128 MB。如果你不设置超过限制的 Excel 上传会直接报 413 或 400前端拿到一个看不懂的错误。我一般在Program.cs里统一配置builder.WebHost.ConfigureKestrel(options { options.Limits.MaxRequestBodySize 50 * 1024 * 1024; }); builder.Services.ConfigureFormOptions(options { options.MultipartBodyLengthLimit 50 * 1024 * 1024; });异常处理我倾向用中间件或过滤器统一拦截而不是在每个 Action 里写 try-catch。Excel 解析的异常类型很多文件损坏、列名不匹配、类型转换失败、密码保护等等。统一返回{ success false, message ex.Message }前端处理起来心里有底。这里不需要把异常内容包装得花里胡哨但一定要把业务含义说清楚方便调用方排查。4. 生产环境实测问题速查4.1 EPPlus 的 License 异常线上项目最常遇到的还有这个开发机没报错部署到服务器就抛异常。原因往往是开发阶段有人把LicenseContext设置在调试代码里换环境后没生效。请记住它是在进程启动时设置的全局配置任何一个能跑起来的入口都要覆盖到。Program.cs里设置是最稳的单元测试里如果用到 EPPlus测试类也要单独设置。4.2 公式单元格读不出值ExcelDataReader 默认读取的是 Excel 文件里“缓存的计算结果”不是公式本身。也就是说单元格里写的是SUM(A1:A5)你读出来的是它上次保存时算好的数字。对大多数业务导入场景这反而是好事因为我们通常就想要最终值。真需要拿到公式原文ExcelDataReader 没有直接接口给到我一般方案是解析 xlsx 内部 XML 的f节点或者改用 EPPlus 去读公式模型。如果只是做数据落库直接信任缓存值即可不用折腾。4.3 导出内容被 Excel 当成公式执行这是我特别想强调的安全点。如果导出的某一列值是用户输入的内容而它刚好以、、-、开头Excel 打开时会把它当作公式执行这就是 CSV/Excel 公式注入。权限足够大时这种注入可能被利用来读取或外传数据。处理方式是在写入单元格前检测危险前缀并加一个前导单引号private static readonly char[] DangerousPrefix { , , -, , \t, \r }; private static object SanitizeCell(object value) { if (value is string s s.Length 0 DangerousPrefix.Contains(s[0])) return s; return value; }加单引号后Excel 会把它当文本显示不会执行公式。这个防护在导入场景同样适用外部传进来的 Excel 也可能带公式落库前要按业务规则判断是取缓存值还是做白名单校验。4.4 中文文件名下载变成乱码或文件名被截断导出的文件名如果带中文直接拼 Content-Disposition 很容易出问题。我的建议是永远用File(bytes, contentType, fileName)这个重载ASP.NET Core 框架会自动处理filename*的 RFC 5987 编码浏览器端能正常显示中文文件名。如果你非要手动构造 Content-Disposition请写成filename*UTF-8url-encoded的形式否则中文大概率乱码。4.5 大文件把服务内存拖垮这个问题排查起来最烧脑因为它不是必现的而是数据量到一定程度才爆发。我整理的几条经验典型现象原因解决方案上传 50 MB Excel 时接口非常慢在代码里 Copy 到了 MemoryStream直接传OpenReadStream()给解析器导出 5 万行时内存暴涨GetAsByteArray()在内存里构建完整文件数据量大用临时文件承接导出流前端上传被 413 拦截Kestrel 或 FormOptions 限制未调大按业务调整两个限制值.xls文件中文乱码没有注册 CodePages 编码启动时注册CodePagesEncodingProvider单元格是日期但读到一串数字UseColumnDataType未开启设置为true我还建议大家给读取接口做“行数上限”保护。比如超过 10 万行直接拒绝解析返回“请分批导入”。Excel 本来就不是数据库把它当大批量数据通道用迟早会反噬你。我个人把这套服务跑了快两年最大的感受是导入导出功能看着简单真正决定线上稳不稳的全是这些边角细节。文件流不要多复制一次公式注入一定要防表头要做校验License 要提前确认。把这些基础打牢excel-handler 就能变成一个让业务方省心、让运维也省心的模块。最后再分享一个小技巧所有 Excel 接口都统一返回{ success, message, data }结构前端和后端联调的时候会很省事这也是我重构之后最后悔没早点做的事。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门