向CsvHelper添加自定义字段属性

6

我正在使用优秀的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”显示为两位小数,“hours”显示为三位小数,“pay rate”显示为四位小数,这是所需的集成要求。

现有工作内容

我创建了一个十进制转换器并将其附加到类映射中:

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之间没有区别,但是每小时15.4837美元和每小时15.48美元的工资率之间肯定有区别。 - Steve Friedl
在这种情况下,我可能可以使用四位小数点来处理所有内容(使其简单),但我不确定集成的接收者是否会接受这种方式,但我仍然想找出如何为无法如此简单解决的情况附加自己的属性。 - Steve Friedl
然后将数据输入为文本,并使用正则表达式确保格式正确。然后转换为数字。 - jdweng
1
为什么不直接将标准属性CsvHelper.Configuration.Attributes.TypeConverterAttribute应用于您的模型,而不是使用自定义属性?请参见 https://dotnetfiddle.net/m2nORw。 - dbc
@dbc - 这是一个非常好的答案,完全解决了十进制转换问题。我相信我仍然需要在字段中漫游来处理其他事情,例如根据C#字段名称自动生成标题字段名称(集成需要具有名称中带有空格的标头); 我宁愿通过编程方式而不是使用大量[Name("Regular Pay")]属性来完成它们。 - Steve Friedl
显示剩余2条评论
1个回答

4
与其使用自定义属性,您可以将 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
}

这里是Demo fiddle #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();

注意:

  • 为了简单起见,我使用了一系列 const string 格式代替了十进制格式的 enum

  • 该属性目前仅适用于属性,但可以扩展到字段。

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

轻度测试的示例 fiddle #2 在这里

最后一个备选方案是,您编写的 顺便说一句:每个字段都可以用 [Format("..")] 属性进行注释,但为了得到我想要的零抑制效果,格式字符串是一个三部分的丑陋东西,看起来非常容易出错并且很繁琐难改。

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


1
这看起来非常有前途;我现在正在调查,谢谢。 - Steve Friedl
是的,这比我请求的要好得多,它产生了巨大的影响。就属性而言,我能够将它们简化为[Hours()] [Dollars()]等,这简化了事情,并且我正在扩展它以处理其他非十进制的东西。谢谢。 - Steve Friedl
1
为了增加我的喜悦,事实证明我们根本不需要导入汇率 - 这让我感到惊讶 - 因此,因为我正在编程地插入地图,如果(attr is PayRateAttribute) map.Map(typeof(T), prop, true).Ignore(); 就可以解决问题。再次感谢! - Steve Friedl
你好。第一个链接现在已经404了,是否有新的URL呢?谢谢。 - Andrew Truckle
1
@AndrewTruckle - 呃,看起来整个CsvHelper API文档现在都是“即将推出”状态。该文档已存档在这里:https://web.archive.org/web/20200909181926/https://joshclose.github.io/CsvHelper/api/CsvHelper.Configuration.Attributes/TypeConverterAttribute/. 我会用源代码链接替换失效的文档链接。 - dbc

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接