使用EPPlus读取含1对多嵌套列表结构的Excel并映射为对象的实现方法
用EPPlus读取含一对多关系的Excel并映射到类的方案
前提说明
你给出的Excel结构为典型的主从合并结构:主字段(Col1~Col4)为合并单元格,同一主对象对应多行子数据,分别映射到List1和List2两个子集合属性,示例结构如下:
首先补全你需要的子类定义(可根据实际Excel列的位置调整[Column]特性的索引值):
using System.ComponentModel.DataAnnotations.Schema; // List1对应的子对象 public class SubClass { // 此处列索引对应Excel中List1字段所在的列号,从1开始计数 [Column(5)] public string Sub1Field1 { get; set; } [Column(6)] public int Sub1Field2 { get; set; } } // List2对应的子对象 public class SubCl { // 此处列索引对应Excel中List2字段所在的列号,从1开始计数 [Column(7)] public string Sub2Field1 { get; set; } [Column(8)] public DateTime Sub2Field2 { get; set; } } // 你给出的主对象定义 public class TestObject { [Column(1)] public int Col1 { get; set; } [Column(2)] public int Col2 { get; set; } [Column(3)] public string Col3 { get; set; } [Column(4)] public DateTime Col4 { get; set; } public List<SubClass> List1 { get; set; } = new List<SubClass>(); public List<SubCl> List2 { get; set; } = new List<SubCl>(); }
读取映射核心代码
首先通过NuGet安装EPPlus:dotnet add package EPPlus
注意:EPPlus 5及以上版本用于商业项目时需要申请商业许可,非开源场景请自行确认授权合规
完整读取代码
using OfficeOpenXml; using System.Reflection; public class ExcelReader { // 辅助方法:获取单元格实际值,兼容合并单元格 private static object GetCellValue(ExcelWorksheet sheet, int row, int col) { if (sheet.Cells[row, col].Merge) { var mergeId = sheet.MergedCells[row, col]; var mergeRange = sheet.Cells[mergeId]; return mergeRange.First().Value; } return sheet.Cells[row, col].Value; } // 泛型映射单元格值到对象属性 private static void MapProperty<T>(T obj, PropertyInfo prop, object cellValue) { if (cellValue == null) return; var targetType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType; var convertedValue = Convert.ChangeType(cellValue, targetType); prop.SetValue(obj, convertedValue); } public List<TestObject> ReadTestObjectsFromExcel(string filePath) { var result = new List<TestObject>(); // EPPlus 5+需要设置许可上下文,非商业使用设为NonCommercial即可 ExcelPackage.LicenseContext = LicenseContext.NonCommercial; using (var package = new ExcelPackage(new FileInfo(filePath))) { var worksheet = package.Workbook.Worksheets.First(); // 假设第一行是表头,从第二行开始读取,可根据实际Excel调整起始行号 int startRow = 2; int endRow = worksheet.Dimension.End.Row; TestObject currentMainObj = null; for (int row = startRow; row <= endRow; row++) { // 判断是否为新的主对象:Col1有值说明是新的主数据块 var col1Value = GetCellValue(worksheet, row, 1); if (col1Value != null) { // 把之前的主对象加入结果集 if (currentMainObj != null) { result.Add(currentMainObj); } // 新建当前主对象并映射主字段 currentMainObj = new TestObject(); var mainProperties = typeof(TestObject) .GetProperties() .Where(p => p.GetCustomAttribute<ColumnAttribute>() != null); foreach (var prop in mainProperties) { var colAttr = prop.GetCustomAttribute<ColumnAttribute>(); var cellValue = GetCellValue(worksheet, row, colAttr.Order); MapProperty(currentMainObj, prop, cellValue); } } // 映射当前行的子数据到两个列表 // 映射SubClass到List1 var sub1 = new SubClass(); var sub1Properties = typeof(SubClass).GetProperties().Where(p => p.GetCustomAttribute<ColumnAttribute>() != null); foreach (var prop in sub1Properties) { var colAttr = prop.GetCustomAttribute<ColumnAttribute>(); var cellValue = worksheet.Cells[row, colAttr.Order].Value; MapProperty(sub1, prop, cellValue); } // 可自行加判断:如果子数据全为空则不添加 currentMainObj.List1.Add(sub1); // 映射SubCl到List2 var sub2 = new SubCl(); var sub2Properties = typeof(SubCl).GetProperties().Where(p => p.GetCustomAttribute<ColumnAttribute>() != null); foreach (var prop in sub2Properties) { var colAttr = prop.GetCustomAttribute<ColumnAttribute>(); var cellValue = worksheet.Cells[row, colAttr.Order].Value; MapProperty(sub2, prop, cellValue); } currentMainObj.List2.Add(sub2); } // 把最后一个主对象加入结果集 if (currentMainObj != null) { result.Add(currentMainObj); } } return result; } }
注意事项
- 以上代码默认Excel每行同时包含两个子列表的字段,如果你的Excel是子列表分块存储(比如前N行是List1数据,后M行是List2数据),需要自行调整子数据的映射逻辑
- 可自行添加空值校验逻辑,过滤掉全空的子数据行
- 列索引和起始行号请根据你实际的Excel结构调整
- 数据类型转换出错时可自行添加异常捕获逻辑处理格式错误的单元格值
内容的提问来源于stack exchange,提问作者Tom
相关产品推荐
相关产品推荐

