在处理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
。如果没有提供映射,将尝试基于第一行的值或通过读取ExcelMapper
和ExcelMap
属性自动创建一个。
ExcelMap
类持有Excel行或列到TItem
属性的映射。它指定了一个Header
属性,可以在派生类中重写,默认值为1。MappingDirection
属性告诉映射器应该将行还是列映射到对象。默认情况下,它是行。它还持有一个类型为Dictionary
的Mapping
属性,它持有实际的映射。
如果想提供自己的映射,只需要从ExcelMap
派生并填充Mapping
字典。另一种指示Excel行或列与TItem
属性之间关系的方法是使用属性。要指示代码需要为创建一个映射对象,需要用属性装饰TItem
类。可以为Header
和MappingDirection
属性提供可选值,例如,[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;
}
这个方法将通过反射查找ExcelMap
的CreateMap
函数。如果找到了该方法,将通过调用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
的属性。