65.9K
CodeProject 正在变化。 阅读更多。
Home

EPPlus 的简单 POCO 映射器

starIconstarIconstarIconstarIconstarIcon

5.00/5 (8投票s)

2017年8月26日

CPOL

4分钟阅读

viewsIcon

22853

downloadIcon

441

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>。当没有提供映射时,我们会尝试基于第一行的值自动创建一个,或者通过读取TItemExcelMapperExcelMap属性来创建。

ExcelMap<TItem>类保存了Excel行或列到TItem属性的映射。它指定了一个Header属性,该属性可以在派生类中被覆盖,默认值为1MappingDirection属性告诉映射器是应该将行还是列映射到对象。默认情况下,是行。它还包含一个类型为Dictionary<int, PropertyInfo>Mapping属性,该属性保存实际的映射。

如果你想提供自己的映射,你只需要派生自ExcelMap<TItem>并填充Mapping字典。

指示Excel行或列与TItem属性之间关系的另一种方法是使用属性。要指示代码根据属性为你创建一个映射对象,你需要使用ExcelMapper属性来装饰TItem类。你可以为HeaderMappingDirection属性提供一个可选值,例如,[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属性为空,我们会尝试根据MappingDirectionHeader属性的值自动创建映射。默认情况下,这将是横向映射,Header值为1,结果是代码尝试将第1行的列与所提供对象的属性进行映射。同样,值为VerticalMappingDirection和值为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
© . All rights reserved.