EPPlus 的简单 POCO 映射器





5.00/5 (8投票s)
EPPlus 的简单 POCO 映射器
引言
在我最近的一个项目中,我需要能够读取Excel文件,我偶然发现了一个非常棒的开源库,名为EPPlus
。不幸的是,我找不到任何通用的方法将行或列转换为对象,所以我决定自己创建它们。
Using the Code
使用该代码的主要逻辑封装在ExcelWorksheetExtensions
类中。
在这里,我们有两个public
方法,用于读取一个特定的映射或获取所有返回的映射列表。
public static TItem GetRecord<TItem>(this ExcelWorksheet sheet,
int rowOrColumn, ExcelMap<TItem> map = null)
public static List<TItem> GetRecords<TItem>(this ExcelWorksheet sheet,
ExcelMap<TItem> map = null)
两者都有一个可选参数ExcelMap<TItem>
。当没有提供映射时,我们会尝试基于第一行的值自动创建一个,或者通过读取TItem
的ExcelMapper
和ExcelMap
属性来创建。
ExcelMap<TItem>
类保存了Excel行或列到TItem
属性的映射。它指定了一个Header
属性,该属性可以在派生类中被覆盖,默认值为1
。MappingDirection
属性告诉映射器是应该将行还是列映射到对象。默认情况下,是行。它还包含一个类型为Dictionary<int, PropertyInfo>
的Mapping
属性,该属性保存实际的映射。
如果你想提供自己的映射,你只需要派生自ExcelMap<TItem>
并填充Mapping
字典。
指示Excel行或列与TItem
属性之间关系的另一种方法是使用属性。要指示代码根据属性为你创建一个映射对象,你需要使用ExcelMapper
属性来装饰TItem
类。你可以为Header
和MappingDirection
属性提供一个可选值,例如,[ExcelMapper(MappingDirection = ExcelMappingDirectionType.Vertical, Header = 0)]
表示Excel工作表中没有标题,对象是垂直映射的。
通过装饰属性并提供列索引,可以设置横向映射的列与属性之间的链接。例如,在属性上方使用[ExcelMap(Column = 1)]
意味着该属性映射到Excel工作表的第一个列。同样,如果方向指定为垂直,[ExcelMap(Row = 1)]
表示属性映射到Excel工作表的第一个行。
第三种方法是让代码根据标题行或列的值为你创建映射。这些值将被去除空格,并与TItem
的属性列表进行比较。在这种情况下,将忽略大小写。
如果代码为你创建了映射,它将调用GetMap
函数。
private static ExcelMap<TItem> GetMap<TItem>(ExcelWorksheet sheet)
where TItem : class
{
var method = typeof(ExcelMap<TItem>).GetMethod("CreateMap",
BindingFlags.Static | BindingFlags.NonPublic);
if (method == null)
{
throw new ArgumentNullException(nameof(method),
$"Method CreateMap not found on type {typeof(ExcelMap<TItem>)}");
}
method = method.MakeGenericMethod(typeof(ExcelMap<TItem>));
var map = method.Invoke(null, new object[] { sheet }) as ExcelMap<TItem>;
if (map == null)
{
throw new ArgumentNullException(nameof(map),
$"Map {typeof(ExcelMap<TItem>)} could not be created");
}
return map;
}
此方法通过反射查找ExcelMap<TItem>
的CreateMap
函数。如果找到该方法,我们通过调用MakeGenericMethod
来定义返回类型。接下来,我们调用该方法并提供Excel工作表作为参数。
protected static TMap CreateMap<TMap>(ExcelWorksheet sheet) where TMap : ExcelMap<TItem>
我们首先创建一个TMap
的实例。然后,我们查找TItem
上的ExcelMapper
属性。如果找到,则用相应的值填充映射字典。
var map = Activator.CreateInstance<TMap>();
var type = typeof(TItem);
// Check if we map by attributes or by column header name
var mapper = type.GetCustomAttribute<ExcelMapperAttribute>();
if (mapper != null)
{
// Map by attribute
map.MappingDirection = mapper.MappingDirection;
map.Header = mapper.Header;
type.GetProperties()
.Select(x => new { Property = x,
Attribute = x.GetCustomAttribute<ExcelMapAttribute>() })
.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);
});
}
如果找不到ExcelMapper
属性或Mapping
属性为空,我们会尝试根据MappingDirection
和Header
属性的值自动创建映射。默认情况下,这将是横向映射,Header值为1
,结果是代码尝试将第1行的列与所提供对象的属性进行映射。同样,值为Vertical
的MappingDirection
和值为2的Header
属性(例如)将尝试将Excel工作表第二列中的值与对象的属性进行映射。
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<string>(map.Header, rowOrColumn)
: sheet.GetValue<string>(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<TItem>
方法中,我们调用GetItem<TItem>
并提供映射。
private static TItem GetItem<TItem>(ExcelWorksheet sheet,
int rowOrColumn, ExcelMap<TItem> map)
where TItem : class
{
var item = Activator.CreateInstance<TItem>();
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
的属性。
下面显示了使用六种不同的映射方法的最终结果。
历史
- 2017年8月27日:首次发布
- 2018年2月14日:增加了对垂直映射的支持
- 2023年11月18日:重构为EPPlusFree,现已作为NuGet包提供:AO.EPPlusFree