将自定义字段属性添加到 CsvHelper
Adding custom field attributes to CsvHelper
我正在使用出色的 CsvHelper 库(当前为 v12.2.2)生成 CSV 文件,并且我正在尝试添加我自己的自定义属性以直接在 class 中指定特殊格式。
我正在写的记录看起来像这样(尽管集成需要约 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位,pay rate是4位;集成需要这个。
现在有效
我创建了一个附加到 classmap 的十进制转换器:
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.
}
我卡住的地方是如何将我的自定义属性粘附到 class 地图,我认为代码看起来像这样:
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?
}
}
我已经为此苦思了几个小时,但找不到如何完成它。
您可以将 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
}
演示 fiddle #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
十进制格式。
该属性目前仅针对属性实现,但可以扩展到字段。
可能需要调整代码以正确处理继承层次结构。
经过轻微测试的演示 fiddle #2 here.
作为最后的选择,您写了 旁注:可以使用 [Format("..")]
属性注释每个字段,但要获得我正在寻找的零抑制,格式字符串是一个三段式丑陋的东西,看起来超级容易出错,改起来也很繁琐。
在这种情况下,可以使用如上所示的具有一组固定 public const string
格式的静态 class 来简化代码并避免重复的格式字符串。
我正在使用出色的 CsvHelper 库(当前为 v12.2.2)生成 CSV 文件,并且我正在尝试添加我自己的自定义属性以直接在 class 中指定特殊格式。
我正在写的记录看起来像这样(尽管集成需要约 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位,pay rate是4位;集成需要这个。
现在有效
我创建了一个附加到 classmap 的十进制转换器:
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.
}
我卡住的地方是如何将我的自定义属性粘附到 class 地图,我认为代码看起来像这样:
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?
}
}
我已经为此苦思了几个小时,但找不到如何完成它。
您可以将 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
}
演示 fiddle #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
十进制格式。该属性目前仅针对属性实现,但可以扩展到字段。
可能需要调整代码以正确处理继承层次结构。
经过轻微测试的演示 fiddle #2 here.
作为最后的选择,您写了 旁注:可以使用 [Format("..")]
属性注释每个字段,但要获得我正在寻找的零抑制,格式字符串是一个三段式丑陋的东西,看起来超级容易出错,改起来也很繁琐。
在这种情况下,可以使用如上所示的具有一组固定 public const string
格式的静态 class 来简化代码并避免重复的格式字符串。