Добавление атрибутов настраиваемого поля в CsvHelper

Я использую отличную библиотеку CsvHelper (в настоящее время v12.2.2) для создания файла CSV и пытаюсь добавить свои собственные атрибуты, чтобы указать особое форматирование непосредственно в классе.

Запись, которую я пишу, выглядит так (хотя с ~ 200 числовыми полями, как того требует интеграция):

class PayrollRecord {
    public int EmployeeID { get; set; }

    public decimal RegularPay   { get; set; }
    public decimal RegularHours { get; set; }
    public decimal RegularRate  { get; set; }

    public decimal OvertimePay   { get; set; }
    public decimal OvertimeHours { get; set; }
    public decimal OvertimeRate  { get; set; }

    // many many more
}

и мне нужно убедиться, что Pay написано с двумя десятичными знаками, часы - с 3, а ставка выплаты - с 4; этого требует интеграция.

Что работает сейчас

Я создал десятичный преобразователь, который прикрепляю к карте классов:

using CsvHelper;
using CsvHelper.TypeConversion;

    // convert decimal to the given number of places, and zeros are
    // emitted as blank.
    public abstract class MyDecimalConverter : DefaultTypeConverter
    {
        protected virtual string getFormat() => "";

        public override string ConvertToString(object value, IWriterRow row, MemberMapData memberMapData)
        {
            if (value is decimal d)
                return (d == 0) ? string.Empty : string.Format(getFormat(), d);

            return base.ConvertToString(value, row, memberMapData);
        }
    }

    public class DollarsConverter : MyDecimalConverter
    {
        protected override string getFormat() => "{0:0.00}";  // 2 decimal places
    }
    public class HoursConverter : MyDecimalConverter
    {
        protected override string getFormat() => "{0:0.000}"; // 3 decimal places
    }
    public class PayRateConverter : MyDecimalConverter
    {
        protected override string getFormat() => "{0:0.0000}"; // 4 decimal places
    }

а затем я применяю их, когда создаю писателя:

    CsvWriter Writer = new CsvWriter( /* stuff */ );

    var classMap = new DefaultClassMap<PayrollRecord>();
    classMap.AutoMap();

    classMap.Map(m => m.RegularPay).TypeConverter<DollarsConverter>();
    classMap.Map(m => m.RegularHours).TypeConverter<HoursConverter>();
    classMap.Map(m => m.RegularRate).TypeConverter<PayRateConverter>();

    classMap.Map(m => m.OvertimePay).TypeConverter<DollarsConverter>();
    classMap.Map(m => m.OvertimeHours).TypeConverter<HoursConverter>();
    classMap.Map(m => m.OvertimeRate).TypeConverter<PayRateConverter>();

    // many more

    Writer.Configuration.RegisterClassMap(classMap);
    ...

Это делает все правильно, но плохо масштабируется: с ~ 200 полями будет сложно поддерживать синхронизацию сопоставления с фактическими определениями полей, и я очень ожидаю, что запись структуру, которую нужно изменить, пока мы не добьемся интеграции.

Боковое примечание: можно аннотировать каждое поле атрибутом [Format("..")], но чтобы получить искомое подавление нуля, строка формата представляет собой уродливую вещь из трех частей, которая выглядит очень легко ошибиться и очень утомительно для изменения.

Что бы я хотел

Я хотел бы создать свои собственные настраиваемые атрибуты, которые я могу применить к каждому члену поля, чтобы указать это, чтобы это выглядело примерно так:

// custom attribute
public enum NumericType { Dollars, Hours, PayRate };

public class DecimalFormatAttribute : System.Attribute
{
    public NumericType Type { get; }

    public DecimalFormatAttribute(NumericType t) => Type = t;
}

// then later
class PayrollRecord {

   [DecimalFormat(NumericType.Dollars)] public decimal RegularPay { get; set; }
   [DecimalFormat(NumericType.Hours)]   public decimal RegularHours { get; set; }
   [DecimalFormat(NumericType.PayRate)] public decimal RegularRate { get; set; }

   // etc.
}

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

    var classMap = new DefaultClassMap<PayrollRecord>();
    classMap.AutoMap();

    foreach (var prop in typeof(PayrollRecord).GetProperties())
    {
        var myattr = (DecimalFormatAttribute)prop.GetCustomAttribute(typeof(DecimalFormatAttribute));

        if (myattr != null)
        {
            // prop.Name is the base name of the field
            // WHAT GOES HERE?
        }
    }

Я ковылял с этим несколько часов и не могу найти, как это сделать.


person Steve Friedl    schedule 23.12.2019    source источник
comment
Почему тебя волнует, что написано? Разве вы не хотите просто форматировать при выводе? В чем разница между 1.0 и 1.0000 при вводе данных?   -  person jdweng    schedule 23.12.2019
comment
Очевидно, что нет разницы между 1,0 и 1,0000, но, безусловно, есть разница между ставкой заработной платы 15,4837 / час и 15,48 / час.   -  person Steve Friedl    schedule 23.12.2019
comment
... и в этом случае я, вероятно, мог бы обойтись четырьмя десятичными знаками для всего (что упрощает), хотя я не уверен, примет ли получатель интеграции это таким образом, но я все же хотел бы выяснить, как прикрепить свои собственные атрибуты к случаям, которые не могут быть решены так просто.   -  person Steve Friedl    schedule 23.12.2019
comment
Затем введите данные как текст и используйте регулярное выражение, чтобы убедиться, что формат правильный. Затем преобразовать в число.   -  person jdweng    schedule 23.12.2019
comment
Это не отвечает на вопрос этого поста: как прикрепить настраиваемые атрибуты к карте классов в CsvHelper. Десятичные разряды - не единственное, что мне нужно сделать.   -  person Steve Friedl    schedule 23.12.2019
comment
Почему бы вместо вашего собственного настраиваемого атрибута просто не применить стандартный атрибут CsvHelper.Configuration.Attributes.TypeConverterAttribute к вашей модели? См. dotnetfiddle.net/m2nORw.   -  person dbc    schedule 23.12.2019
comment
@dbc - это отличный ответ, полностью решающий проблему десятичного преобразования. Я считаю, что мне все еще нужно бродить по полям, чтобы обрабатывать другие вещи, такие как автоматическое создание имен полей заголовков на основе имени поля C # (для интеграции требуются заголовки с пробелами в их именах); Я бы предпочел делать их программно, чем с кучей [Name("Regular Pay")] атрибутов.   -  person Steve Friedl    schedule 23.12.2019


Ответы (1)


Вместо собственного настраиваемого атрибута вы можете применить CsvHelper.Configuration.Attributes.TypeConverterAttribute к вашей модели, чтобы указать соответствующий преобразователь:

class PayrollRecord 
{
    public int EmployeeID { get; set; }

    [TypeConverter(typeof(DollarsConverter))]
    public decimal RegularPay   { get; set; }
    [TypeConverter(typeof(HoursConverter))]
    public decimal RegularHours { get; set; }
    [TypeConverter(typeof(PayRateConverter))]
    public decimal RegularRate  { get; set; }

    [TypeConverter(typeof(DollarsConverter))]
    public decimal OvertimePay   { get; set; }
    [TypeConverter(typeof(HoursConverter))]
    public decimal OvertimeHours { get; set; }
    [TypeConverter(typeof(PayRateConverter))]
    public decimal OvertimeRate  { get; set; }

    // many many more
}

Демо-скрипт №1 здесь.

В качестве альтернативы, если вы не хотите применять атрибуты CsvHelper к своей модели данных, вы можете использовать настраиваемый атрибут следующим образом:

public static class NumericType
{
    public const string Dollars = "{0:0.00}";
    public const string Hours = "{0:0.000}";
    public const string PayRate = "{0:0.0000}";
}

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class DecimalFormatAttribute : System.Attribute
{
    public string Format { get; } = "{0}";

    public DecimalFormatAttribute(string format) => Format = format;
}

public class MyDecimalConverter : DefaultTypeConverter
{
    public string Format { get; } = "{0}";

    public MyDecimalConverter(string format) => Format = format;

    public override string ConvertToString(object value, IWriterRow row, MemberMapData memberMapData)
    {
        if (value is decimal d)
            return (d == 0) ? string.Empty : string.Format(Format, d);

        return base.ConvertToString(value, row, memberMapData);
    }
}

public static class CsvHelpExtensions
{
    public static void RegisterDecimalFormats<T>(this ClassMap<T> map)
    {
        foreach (var property in typeof(T).GetProperties())
        {
            var attr = property.GetCustomAttribute<DecimalFormatAttribute>();
            if (attr != null)
                map.Map(typeof(T), property, true).TypeConverter(new MyDecimalConverter(attr.Format));
        }
    }
}

Что можно применить следующим образом:

class PayrollRecord 
{
    public int EmployeeID { get; set; }

    [DecimalFormat(NumericType.Dollars)]
    public decimal RegularPay   { get; set; }
    [DecimalFormat(NumericType.Hours)]
    public decimal RegularHours { get; set; }
    [DecimalFormat(NumericType.PayRate)]
    public decimal RegularRate  { get; set; }

    [DecimalFormat(NumericType.Dollars)]
    public decimal OvertimePay   { get; set; }
    [DecimalFormat(NumericType.Hours)]
    public decimal OvertimeHours { get; set; }
    [DecimalFormat(NumericType.PayRate)]
    public decimal OvertimeRate  { get; set; }

    // many many more
}

И используется следующим образом:

var classMap = new DefaultClassMap<PayrollRecord>();
classMap.AutoMap(); // Do this before RegisterDecimalFormats
classMap.RegisterDecimalFormats();

Примечания:

  • Вместо enum для десятичных форматов я для простоты использовал серию const string форматов.

  • Атрибут в настоящее время реализован только для свойств, но может быть расширен до полей.

  • Возможно, потребуется настроить код для правильной обработки иерархий наследования.

Слегка протестированная демонстрационная скрипта № 2 здесь.

В качестве последней альтернативы вы написали Боковое примечание: можно аннотировать каждое поле атрибутом [Format("..")], но для получения искомого подавления нуля строка формата представляет собой уродливую вещь из трех частей, которая выглядит очень простой ошибиться и очень утомительно менять.

В такой ситуации можно использовать статический класс с фиксированным набором форматов public const string, как показано выше, для упрощения кода и предотвращения дублирования строк формата.

person dbc    schedule 23.12.2019
comment
Это выглядит супер многообещающим; исследуем сейчас, спасибо. - person Steve Friedl; 23.12.2019
comment
Да, это было намного лучше, чем я просил, это имело огромное значение. Что касается атрибута, я смог сделать их проще, например [Hours()] [Dollars()] и т. Д., Что упрощает вещи, и я расширяю его для обработки других недесятичных вещей. Спасибо. - person Steve Friedl; 23.12.2019
comment
Просто чтобы добавить к моему восторгу; оказывается, что нам вообще не нужно импортировать ставки - что меня удивило - поэтому, поскольку я программно подключаюсь к карте, if (attr is PayRateAttribute) map.Map(typeof(T), prop, true).Ignore(); делает свое дело. Спасибо еще раз! - person Steve Friedl; 24.12.2019