【问题标题】:Adding custom field attributes to CsvHelper向 CsvHelper 添加自定义字段属性
【发布时间】:2019-12-23 18:13:41
【问题描述】:

我正在使用出色的 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 用小数点后 2 位写入,小时用 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?
        }
    }

我已经为此苦苦琢磨了几个小时,但不知道如何完成。

【问题讨论】:

  • 你为什么在乎写什么?您不想在输出时格式化吗?输入数据时1.0和1.0000有什么区别?
  • 1.0 和 1.0000 之间显然没有区别,但工资率 15.4837/hr 和 15.48/hr 之间肯定有区别。
  • ... 在这种情况下,我可能会为所有内容保留四位小数(使其变得简单),尽管我不确定集成的接收者是否会接受这种方式,但我仍然想弄清楚如何为无法简单解决的情况附加我自己的属性。
  • 然后以文本形式输入数据并使用正则表达式确保格式正确。然后转换为数字。
  • 而不是您自己的自定义属性,为什么不将标准属性CsvHelper.Configuration.Attributes.TypeConverterAttribute 应用于您的模型?见dotnetfiddle.net/m2nORw

标签: c# csvhelper


【解决方案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 here.

或者,如果您不想将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();

注意事项:

  • 为了简单起见,我使用了一系列const string 格式,而不是enum 用于十进制格式。

  • 该属性目前仅针对属性实现,但可以扩展到字段。

  • 可能需要调整代码以正确处理继承层次结构。

经过轻微测试的演示小提琴 #2 here。

作为最后的选择,您写了 旁注:可以使用 [Format("..")] 属性注释每个字段,但要获得我正在寻找的零抑制,格式字符串是由三部分组成的丑陋看起来超级容易出错且更改非常繁琐的事情。

在这种情况下,可以使用如上所示具有一组固定public const string 格式的静态类来简化代码并避免重复的格式字符串。

【讨论】:

  • 这看起来超级很有希望;现在调查,谢谢。
  • 是的,这比我要求的要好得多,它有很大的不同。就属性而言,我能够将它们简化为 [Hours()] [Dollars()] 等,这简化了事情,并将其扩展到处理其他非小数的事情。谢谢。
  • 只是为了增加我的喜悦;事实证明,我们根本不需要导入费率——这让我感到惊讶——所以因为我以编程方式连接到地图中,if (attr is PayRateAttribute) map.Map(typeof(T), prop, true).Ignore(); 可以解决问题。再次感谢!
  • 嗨。现在 404 的第一个链接是否有新的 URL?谢谢。
  • @AndrewTruckle - 嗯,看起来整个CsvHelper API documentation 现在“即将推出”。该文档存档在这里:web.archive.org/web/20200909181926/https://joshclose.github.io/…。我将用源代码链接替换死文档链接。
猜你喜欢
  • 2015-05-03
  • 2015-03-09
  • 2018-08-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多