NullValues选项在加载到数据表时不起作用

时间:2019-02-06 00:20:22

标签: c# .net csvhelper

在将CSV读取到DataTable中时,我试图为似乎无效的布尔值和null值添加选项。例如,一个文件包含类似于以下内容的数据:

Id,MaxDiscount,Name,Active,AltId
1,,Foo,1,ABC123
2,10,Bar,0,DEF345

以及以下使用架构文件动态获取我们期望的标头和数据类型的逻辑:

var dt = new DataTable();
using (var reader = new StreamReader(file.FullName))
using (var csv = new CsvReader(reader))
{
    csv.Configuration.HasHeaderRecord = true;
    csv.Configuration.IgnoreQuotes = false;
    csv.Configuration.TypeConverterOptionsCache.GetOptions<int>().NullValues.Add(string.Empty);
    csv.Configuration.TypeConverterOptionsCache.GetOptions<bool>().BooleanFalseValues.Add("0");
    csv.Configuration.TypeConverterOptionsCache.GetOptions<bool>().BooleanTrueValues.Add("1");

    using (var dr = new CsvDataReader(csv))
    {
        foreach (var p in schema.Properties)
        {
            var type = Type.GetType(p.Type, true, true);
            var dc = new DataColumn
            {
                ColumnName = p.Name,
                Unique = p.IsId,
                AllowDBNull = p.Nullable,
                DataType = type
            };

            dt.Columns.Add(dc);
        }
        dt.Load(dr);
    }
}

这会导致错误String was not recognized as a valid Boolean. Couldn't store <0> in Active Column. Expected type is Boolean.

如果我手动更改数据并将0替换为false1替换为true,则布尔值可以工作,但是会出现类似的错误:{ {1}}

为了使此功能正常工作,我在这里缺少什么吗?还是类型转换器选项仅适用于已知对象?

编辑:

在解析CSV文件时,我无法使用任何预定义的对象模型,因为它们可以包含任意数量的字段。只要存在模式,程序就应该知道如何处理它。模式示例如下所示:

Input string was not in a correct format. Couldn't store <> in MaxDiscount Column.  Expected type is Int32.

在这种情况下,架构中的{ "type": "Part", "bucket": "s3Bucket", "prefix": "prefix/of/datafile", "targetDirectory": "..\\path\\to\\working\\dir", "delimiter": ",", "properties": [ { "name": "Id", "type": "System.String", "required": true, "nullable": false, "isId": true, "defaultValue": null, "minLength": 6, "maxLength": 8 }, { "name": "MaxDiscount", "type": "System.Int32", "required": true, "nullable": true, "isId": false, "defaultValue": null, "minLength": -1, "maxLength": -1 }, { "name": "Name", "type": "System.String", "required": true, "nullable": false, "isId": false, "defaultValue": null, "minLength": 1, "maxLength": 127 }, { "name": "Active", "type": "System.Boolean", "required": true, "nullable": false, "isId": false, "defaultValue": null, "minLength": 1, "maxLength": 1 }, { "name": "AltId", "type": "System.String", "required": true, "nullable": true, "isId": false, "defaultValue": null, "minLength": 1, "maxLength": 127 } ] } 将与CSV文件中的列相关。从理论上讲,这将使我能够在运行时解析文件并验证数据类型,而不必每次引入新的CSV布局时都创建一个新的对象模型。

3 个答案:

答案 0 :(得分:3)

我认为CsvDataReader类没有用-GetFieldType的实现返回typeof(string)GetValue也返回string,因此尽管它实现了类型的数据访问器方法,它们永远不会被DataTableLoad方法调用。

因此没有CsvHelper映射发生-转换是由DataTable使用标准字符串类型转换器进行的。

我建议删除CsvDataReader类的用法,并用以下内容代替dt.Load(dr);调用:

static void Load(DataTable dt, CsvReader csv)
{
    if (csv.Configuration.HasHeaderRecord)
    {
        if (!csv.Read()) return;
        csv.ReadHeader();
    }
    var valueTypes = new Type[dt.Columns.Count];
    for (int i = 0; i < valueTypes.Length; i++)
    {
        var dc = dt.Columns[i];
        var type = dc.DataType;
        if (dc.AllowDBNull && type.IsValueType)
            type = typeof(Nullable<>).MakeGenericType(type);
        valueTypes[i] = type;
    }
    var valueBuffer = new object[valueTypes.Length];
    dt.BeginLoadData();
    while (csv.Read())
    {
        for (int i = 0; i < valueBuffer.Length; i++)
            valueBuffer[i] = csv.GetField(valueTypes[i], i);
        dt.LoadDataRow(valueBuffer, true);
    }
    dt.EndLoadData();
}

基本上准备列类型映射,并使用CsvReader.GetField(type, index)方法填充DataRow值。这样,转换由CsvReader类执行,并将使用所有转换选项。

顺便说一句,实际上不需要显示的布尔值或null值的选项-所有它们都由CsvHelper默认类型转换器处理。

答案 1 :(得分:1)

来自CsvHelper documentation

  

如果要指定列和列类型,数据表将以自动转换的类型加载。

使用CsvReader时,我忽略了CsvDataReader类型转换器选项。

但是,如果您使用csv.GetRecords,它将使用已定义的类型转换器选项。

List<csvData> result = csv.GetRecords<csvData>().ToList();

您需要将csv文件作为类,如下所示

public class csvData
{
    public int Id { get; set; }
    public string MaxDiscount { get; set; }
    public string Name { get; set; }
    public bool Active { get; set; }
    public string AltId { get; set; }
}

答案 2 :(得分:1)

[第二次尝试]

只要DataTable创建了CsvDataReader的集合并将DataColumns设置为CsvDataReader,就可以通过Configuration.Delimiter将数据加载到Active对象中逗号,但...布尔字段(BooleanFalseValuesAttribute)并不是真正的布尔值。

根据我的测试和对文档的理解,只有一种方法来获取正确的数据-通过helper类,该类需要将attributes设置为字段。其中两个非常重要:

  

BooleanTrueValuesAttribute用来表示字符串的字符串值   布尔值转换时为false。   public class MyData { [Name("Id")] public int Id { get; set; } [Name("MaxDiscount")] public int? MaxDiscount { get; set; } [Name("Name")] public string Name { get; set; } [Name("Active")] [BooleanTrueValues("1")] [BooleanFalseValues("0")] public bool? Active { get; set; } [Name("AltId")] public string AltId { get; set; } } 转换时用于表示布尔值true的字符串值。

因此,类的装饰可能类似于:

public class MyDataMapper: ClassMap<MyData>
{
    public MyDataMapper()
    {
        Map(m => m.Id);
        Map(m => m.MaxDiscount);
        Map(m => m.Name);
        Map(m => m.Active);
        Map(m => m.AltId);
    }
}

还有映射字段的帮助程序类:

csv.Configuration.RegisterClassMap<MyDataMapper>();

然后我尝试设置配置:

DataTable

能够通过CsvDataReader对象将数据捕获到CsvDataReader中,但是...没有成功:(

似乎GetRecords<T>由于某些原因(或我无法成功设置)而忽略了配置。

每当需要映射字段时,文档都说获取数据的正确方法是使用var records = csv.GetRecords<Foo>(); 方法:

List<MyData> records = null;
using (var reader = new StreamReader(myfile))
using (var csv = new CsvReader(reader))
{
    csv.Configuration.HasHeaderRecord = true;
    csv.Configuration.IgnoreQuotes = false;
    csv.Configuration.Delimiter = ",";
    csv.Configuration.RegisterClassMap<MyDataMapper>();
    records = csv.GetRecords<MyData>().ToList();
    dt = records.Select(x=>dt.LoadDataRow(new object[]
            {
                x.Id,
                x.MaxDiscount,
                x.Name,
                x.Active,
                x.AltId
            },false))
            .CopyToDataTable();
     dt.Dump();

请参阅:Mapping properties

如果我对您的理解很好,您想将数据提取到DataTable对象中……看一下:

Id MaxDiscount Name Active AltId
1  null        Foo  True   ABC123 
2  10          Bar  False  DEF345 

结果是:

  output$race_standings_table = DT::renderDataTable({
    standings %>%
      filter(year == input$round,
             name..35 == input$race_name)

  })