我将出色的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位,hours的小时数是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?
}
}
我已经忙了好几个小时,却找不到如何完成这项工作。
答案 0 :(得分:2)
您可以将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();
注意:
为了简单起见,我使用了一系列enum
格式,而不是十进制格式的const string
。
该属性当前仅针对属性实现,但可以扩展到字段。
可能需要调整代码以正确处理继承层次结构。
经过严格测试的演示小提琴#2 here。
作为最后的选择,您写了边注:可以使用[Format("..")]
属性来注释每个字段,但是要获得我正在寻找的零抑制,格式字符串是三个-部分看起来很丑陋的东西,很容易出错,而且非常乏味。
在这种情况下,可以使用具有固定的public const string
格式集的静态类(如上所示)来简化代码并避免重复的格式字符串。