Как расширить epplus для извлечения данных с помощью метода, подобного WithAllProperties - PullRequest
0 голосов
/ 05 февраля 2019

Я работаю с файлами Excel.Я должен прочитать табличные значения, хранящиеся в таблицах Excel, и десериализовать их в объекты.Для этого я использую OfficeOpenXml с помощью EPPlus.DataExtractor.Мои таблицы Excel имеют несколько столбцов, поэтому у моих классов есть несколько свойств - с разными типами данных: строки, целые числа, DateTimes, двойные и пустые числа, DateTimes, double.Я не могу предположить, что больше типов не придет вовремя.Например, класс для десериализации строки Excel может выглядеть следующим образом:

public class MyModel
{
    [Column("A")]
    public string Id { get; set; }
    [Column("B")]
    public string Code { get; set; }
    [Column("C")]
    public int Number { get; set; }
    [Column("D")]
    public DateTime? ValidTo { get; set; }
}

Столбец - это мой собственный атрибут, который сообщает экстрактору, в каком столбце содержится значение для данного свойства:

public class ColumnAttribute : Attribute
{
    public string Column { get; set; }
    public ColumnAttribute(string column) => Column = column;
}

Вот почему я могу использовать EPPlus следующим образом

public class MyModelExtractor
{
    private readonly string _path;
    public MyModelExtractor(string path) => _path = path;

    public List<MyModel> Create()
    {
        using (var excelPackage = new ExcelPackage(new FileInfo(_path)))
        {
            var worksheet = excelPackage.Workbook.Worksheets[1];
            return worksheet
                .Extract<MyModel>()
                .WithProperty(p => p.Id, MyModel.GetColumnAnnotation(p => p.Id))
                .WithProperty(p => p.Code , MyModel.GetColumnAnnotation(p => p.Code ))
                .WithProperty(p => p.Number, MyModel.GetColumnAnnotation(p => p.Number))
                .WithProperty(p => p.ValidTo , MyModel.GetColumnAnnotation(p => p.ValidTo ))
                .GetData(2, row => worksheet.Cells[row, 1].Value != null)
                .ToList();
        }
    }

Теперь в классе MyModel есть нечто большее, а именно:

public static string GetColumnAnnotation<T>(Expression<Func<MyModel, T>> propertySelector) =>
        AttributeExtractor.GetPropertyAttributeValue<MyModel, T, ColumnAttribute, string>(propertySelector, attribute => attribute.Column);

, который, как видно, используетсяв методе WithProperty, чтобы получить значение атрибута Column (просто строка).

Для полноты я предоставлю AttributeExtractor, который выглядит следующим образом:

public static class AttributeExtractor
{
    public static TValue GetPropertyAttributeValue<T, TOut, TAttribute, TValue>(Expression<Func<T, TOut>> propertyExpression,
        Func<TAttribute, TValue> valueSelector) where TAttribute : Attribute
    {
        var propertyInfo = (PropertyInfo)((MemberExpression)propertyExpression.Body).Member;
        return propertyInfo.GetCustomAttributes(typeof(TAttribute), true).FirstOrDefault() is TAttribute attr
            ? valueSelector(attr)
            : throw new MissingMemberException(typeof(T).Name + "." + propertyInfo.Name, typeof(TAttribute).Name);
    }
}

Теперь в каждой моделикласс (и у меня их десятки) я должен предоставить этот статический метод GetPropertyAttributeValue.Что еще более проблематично, классы содержат много свойств, поэтому вызов WithProperty выполняется много раз.И, опять же, для каждого класса у меня есть отдельный соответствующий экстрактор.

Я думал о создании универсальной версии Extractor, например,

public class Extractor<T> { ... }

, где T будет типом, подобным MyModel, и затем я мог бы написать некоторый метод, например WithAllProperties (), который заменитвсе вызовы WithProperty.

Тогда класс будет выглядеть так:

public class Extractor<T> 
{
    ...ctor and _path, and then:
    public List<T> Create()
    {
        using (var excelPackage = new ExcelPackage(new FileInfo(_path)))
        {
            var worksheet = excelPackage.Workbook.Worksheets[1];
            return worksheet
                .Extract<T>()
                .WithAllProperties()
                .GetData(2, row => worksheet.Cells[row, 1].Value != null)
                .ToList();
        }
    }
}

Теперь я борюсь с методом WithAllProperties.Это должно выглядеть так:

public static ICollectionPropertyConfiguration<T> WithAllProperties<T>(
        this IDataExtractor<T> extractor) where T : class, new()
    {
       foreach(var property in typeof(T).GetProperties())
            extractor = extractor.WithProperty(/1/, /2/);

        return extractor as ICollectionPropertyConfiguration<T>;
    }

Чего не хватает, так это / 1 /, который должен иметь тип

Expression<Func<T,TProperty>> 

Я не могу динамически генерировать это значение (без некоторых уловок, которые кажутся непонятными длямне, как переключение типа переменной свойства, и создание необходимого выражения. Это работает, но когда появляются новые типы, этот переключатель должен быть расширен, и я уверен, что это может быть сделано динамически с помощью отражения).Другая вещь - это / 2 /, которая является значением атрибута Column для соответствующего свойства - для этого я не знаю, как его получить.

Нужна любая помощь / подсказки / подсказки.

1 Ответ

0 голосов
/ 08 февраля 2019

Хорошо, поскольку никто не успел дать никаких подсказок, я разработал собственное решение.Это не идеально - возможно, его можно улучшить, но я доволен результатами, так как мой код значительно уменьшился.

Первое изменение заключалось в том, чтобы очистить все модели Excel, такие как MyModel, от статического метода GetColumnAnnotation.То, что осталось, это чистые свойства с атрибутом Column.Следующим изменением было избавиться от универсального AttributeExtractor - он больше не нужен.

Я создал общий класс ExcelExtractor, который выглядит очень тонким:

public class ExcelExtractor<T> where T: class, new()
{
    public ExcelExtractor(IExcelPathProvider pathProvider) => _pathProvider = pathProvider;
    public List<T> Create(int sheetNumber)
    {
        using (var excelPackage = new ExcelPackage(new FileInfo(_pathProvider.GetPath())))
        {
            var worksheet = excelPackage.Workbook.Worksheets[sheetNumber];
            return worksheet
                .Extract<T>()
                .WithAllProperties()
                .GetData(2, row => worksheet.Cells[row, 1].Value != null)
                .ToList();
        }
    }

    private readonly IExcelPathProvider _pathProvider;
}

Затем я создал некоторый класс расширения, который выглядит следующим образом:

public static class ReflectionExtensions
{
    public static ICollectionPropertyConfiguration<T> WithAllProperties<T>(
        this IDataExtractor<T> extractor) where T : class, new() =>
        typeof(T)
        .GetProperties()
        .Aggregate(extractor, ExtractProperty) as ICollectionPropertyConfiguration<T>;

    private static string ToColumn(this PropertyInfo property) =>
        ((ColumnAttribute)property.GetCustomAttributes(typeof(ColumnAttribute), true)
            .First()).Column;
    private static IDataExtractor<T> ExtractProperty<T>(IDataExtractor<T> extractor,
        PropertyInfo property) where T : class, new()
    {
        if (property.PropertyType == typeof(string))
            return extractor.WithProperty(ExpressionGenerator<T>.GetStringProperty(property), property.ToColumn());

        if (property.PropertyType == typeof(int))
            return extractor.WithProperty(ExpressionGenerator<T>.GetIntProperty(property), property.ToColumn());

        if (property.PropertyType == typeof(int?))
            return extractor.WithProperty(ExpressionGenerator<T>.GetNullableIntProperty(property), property.ToColumn());

        if (property.PropertyType == typeof(DateTime))
            return extractor.WithProperty(ExpressionGenerator<T>.GetDateTimeProperty(property), property.ToColumn());

        if (property.PropertyType == typeof(DateTime?))
            return extractor.WithProperty(ExpressionGenerator<T>.GetNullableDateTimeProperty(property), property.ToColumn());

        if (property.PropertyType == typeof(bool))
            return extractor.WithProperty(ExpressionGenerator<T>.GetBooleanProperty(property), property.ToColumn());

        if (property.PropertyType == typeof(bool?))
            return extractor.WithProperty(ExpressionGenerator<T>.GetNullableBooleanProperty(property), property.ToColumn());

        throw new ArgumentException($"Unknown type {property.PropertyType}");
    }
    private static class ExpressionGenerator<T>
    {
        public static Expression<Func<T, string>> GetStringProperty(PropertyInfo property) =>
            Expression.Lambda<Func<T, string>>(GetMember(property), Parameter);
        public static Expression<Func<T, int>> GetIntProperty(PropertyInfo property) =>
            Expression.Lambda<Func<T, int>>(GetMember(property), Parameter);
        public static Expression<Func<T, int?>> GetNullableIntProperty(PropertyInfo property) =>
            Expression.Lambda<Func<T, int?>>(GetMember(property), Parameter);
        public static Expression<Func<T, DateTime>> GetDateTimeProperty(PropertyInfo property) =>
            Expression.Lambda<Func<T, DateTime>>(GetMember(property), Parameter);
        public static Expression<Func<T, DateTime?>> GetNullableDateTimeProperty(PropertyInfo property) =>
            Expression.Lambda<Func<T, DateTime?>>(GetMember(property), Parameter);
        public static Expression<Func<T, bool>> GetBooleanProperty(PropertyInfo property) =>
            Expression.Lambda<Func<T, bool>>(GetMember(property), Parameter);
        public static Expression<Func<T, bool?>> GetNullableBooleanProperty(PropertyInfo property) =>
            Expression.Lambda<Func<T, bool?>>(GetMember(property), Parameter);

        private static readonly ParameterExpression Parameter =
            Expression.Parameter(typeof(T), "p");
        private static MemberExpression GetMember(PropertyInfo property) =>
            Expression.Property(Parameter, property.Name);

    }
}

Вероятно, решениеможет быть улучшен еще больше, и я буду очень признателен, если кто-нибудь даст какие-то советы, но я очень доволен результатами - у меня есть универсальный экстрактор, который работает как шарм для любых новых моделей Excel, которые я когда-либо смогу создать.Если потребуется какой-то дополнительный тип данных, я добавлю два метода в свой класс помощника.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...