Mapping Excel Rows and Columns to Objects in C#

在处理Excel文件时,经常需要将数据映射到对象中。最近,遇到了一个优秀的开源库叫做EPPlus。遗憾的是,没有找到任何通用的方法来将行或列转换为对象,所以决定自己创建这些方法。

使用这个代码的主要逻辑被封装在名为ExcelWorksheetExtensions的类中。这里,有两个公共方法,一个用于读取特定的映射,另一个用于获取所有映射并返回一个列表。

C#代码示例: public static TItem GetRecord( this ExcelWorksheet sheet, int rowOrColumn, ExcelMap map = null ) public static List GetRecords( this ExcelWorksheet sheet, ExcelMap map = null ) 这两个方法都有一个可选参数ExcelMap。如果没有提供映射,将尝试基于第一行的值或通过读取ExcelMapperExcelMap属性自动创建一个。

ExcelMap类持有Excel行或列到TItem属性的映射。它指定了一个Header属性,可以在派生类中重写,默认值为1。MappingDirection属性告诉映射器应该将行还是列映射到对象。默认情况下,它是行。它还持有一个类型为DictionaryMapping属性,它持有实际的映射。

如果想提供自己的映射,只需要从ExcelMap派生并填充Mapping字典。另一种指示Excel行或列与TItem属性之间关系的方法是使用属性。要指示代码需要为创建一个映射对象,需要用属性装饰TItem类。可以为HeaderMappingDirection属性提供可选值,例如,[ExcelMapper(MappingDirection = ExcelMappingDirectionType.Vertical, Header = 0)]意味着Excel工作表中没有标题,对象是垂直映射的。

水平映射时,可以通过在属性上装饰ExcelMap属性并提供列索引来设置列与属性之间的联系。例如,[ExcelMap(Column = 1)]表示该属性映射到Excel工作表的第一列。同样,[ExcelMap(Row = 1)]表示如果方向指定为垂直,则属性映射到Excel工作表的第一行。

第三种方法是让代码根据标题行或列的值为创建映射。然后,这些值将被剥离空格,并与TItem的属性列表进行比较。在这种情况下,将忽略大小写。

如果代码为创建了映射,它将调用GetMap函数。 C#代码示例: private static ExcelMap GetMap(ExcelWorksheet sheet) where TItem : class { var method = typeof(ExcelMap).GetMethod("CreateMap", BindingFlags.Static | BindingFlags.NonPublic); if (method == null) { throw new ArgumentNullException(nameof(method), $"Method CreateMap not found on type {typeof(ExcelMap)}"); } method = method.MakeGenericMethod(typeof(ExcelMap)); var map = method.Invoke(null, new object[] { sheet }) as ExcelMap; if (map == null) { throw new ArgumentNullException(nameof(map), $"Map {typeof(ExcelMap)} could not be created"); } return map; } 这个方法将通过反射查找ExcelMapCreateMap函数。如果找到了该方法,将通过调用MakeGenericMethod定义返回类型。接下来,调用该方法并提供Excel工作表作为参数。

C#代码示例: protected static TMap CreateMap(ExcelWorksheet sheet) where TMap : ExcelMap { var map = Activator.CreateInstance(); var type = typeof(TItem); // Check if we map by attributes or by column header name var mapper = type.GetCustomAttribute(); if (mapper != null) { // Map by attribute map.MappingDirection = mapper.MappingDirection; map.Header = mapper.Header; type.GetProperties() .Select(x => new { Property = x, Attribute = x.GetCustomAttribute() }) .Where(x => x.Attribute != null) .ToList() .ForEach(prop => { var key = map.MappingDirection == ExcelMappingDirectionType.Horizontal ? prop.Attribute.Column : prop.Attribute.Row; map.Mapping.Add(key, prop.Property); }); } if (!map.Mapping.Any()) { // Map by column / row header name var props = type.GetProperties().ToList(); // Determine end dimension for the header var endDimension = map.MappingDirection == ExcelMappingDirectionType.Horizontal ? sheet.Dimension.End.Column : sheet.Dimension.End.Row; for (var rowOrColumn = 1; rowOrColumn <= endDimension; rowOrColumn++) { var parameter = map.MappingDirection == ExcelMappingDirectionType.Horizontal ? sheet.GetValue(map.Header, rowOrColumn) : sheet.GetValue(rowOrColumn, map.Header); if (string.IsNullOrWhiteSpace(parameter)) { var message = map.MappingDirection == ExcelMappingDirectionType.Horizontal ? $"Column {rowOrColumn} has no parameter name" : $"Row {rowOrColumn} has no parameter name"; throw new ArgumentNullException(nameof(parameter), message); } // Remove spaces parameter = parameter.Replace(" ", string.Empty).Trim(); // Map to property var prop = props.FirstOrDefault(x => StringComparer.OrdinalIgnoreCase.Equals(x.Name, parameter)); if (prop == null) { throw new ArgumentNullException(nameof(parameter), $"No property {parameter} found on type {typeof(TItem)}"); } map.Mapping.Add(rowOrColumn, prop); } } }

现在有了映射对象,无论是直接提供的还是由代码创建的,接下来需要做的是创建TItem对象并用实际值填充它。在GetRecords方法中,调用GetItem并提供映射。

C#代码示例: private static TItem GetItem(ExcelWorksheet sheet, int rowOrColumn, ExcelMap map) where TItem : class { var item = Activator.CreateInstance(); foreach (var mapping in map.Mapping) { if ((map.MappingDirection == ExcelMappingDirectionType.Horizontal && mapping.Key > sheet.Dimension.End.Column) || (map.MappingDirection == ExcelMappingDirectionType.Vertical && mapping.Key > sheet.Dimension.End.Row)) { throw new ArgumentOutOfRangeException(nameof(rowOrColumn), $"Key {mapping.Key} is outside of the sheet dimension using direction {map.MappingDirection}"); } var value = (map.MappingDirection == ExcelMappingDirectionType.Horizontal) ? sheet.GetValue(rowOrColumn, mapping.Key) : sheet.GetValue(mapping.Key, rowOrColumn); if (value != null) { // Test nullable var type = mapping.Value.PropertyType.IsValueType ? Nullable.GetUnderlyingType(mapping.Value.PropertyType) ?? mapping.Value.PropertyType : mapping.Value.PropertyType; var convertedValue = (type == typeof(string)) ? value.ToString().Trim() : Convert.ChangeType(value, type); mapping.Value.SetValue(item, convertedValue); } else { // Explicitly set null values to prevent properties being initialized with their default values mapping.Value.SetValue(item, null); } } return item; }

首先使用Activator.CreateInstance创建请求的对象。接下来,遍历映射,并检查提供的列或行是否在Excel工作表的尺寸内。然后从工作表中读取值。为了确定属性的正确类型,检查是否可以为空,通过调用Nullable.GetUnderlyingType。然后用Excel列的值填充TItem的属性。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485