Skip to content

Index Attribute seems not to map to List<T>, but via ClassMap it works #2364

@pbru87

Description

@pbru87

Describe the bug
I tried to read a CSV file whereas starting from the third column all following columns should be mapped to the same List<int?>. At first, I used the below code without ClassMap and just used property attributes (see below).

This resulted in the first two columns being read and mapped correctly, but List<int?>? TagIds was still null, althought the CSV file had values in columns 3 and following. So, the attribute I added to that property ([Index(2, indexEnd: -1)]) is unfortunately not working (as I expected).

Later, I added that ClassMap, which has the same details as the property attributes. When executing csvReader.Context.RegisterClassMap<ExampleCsv.ExampleCsvMap>(); before reading that CSV file, it works and List<int?>? TagIds gets populated with the correct values (the two other props as well, so everything just fine).

Why do both approaches lead to different results? (Seems like a bug to me.)

To Reproduce
I used .NET 9.0 and CsvHelper v33.1.0

Execute this code, to reproduce the bug:

using var reader = new StreamReader(stream, CsvHlpr.DEFAULT_ENCODING_FOR_CSV_IMPORTS);
using var csvReader = CsvHlpr.GetCsvReader(reader);
// Note: Not registering any ClassMap here.

await foreach (var record in csvReader.GetRecordsAsync<ExampleCsv>())
{
    // Do something...
}

Execute this code to make use of the ClassMap (where the bug is not occuring):

using var reader = new StreamReader(stream, CsvHlpr.DEFAULT_ENCODING_FOR_CSV_IMPORTS);
using var csvReader = CsvHlpr.GetCsvReader(reader);
csvReader.Context.RegisterClassMap<ExampleCsv.ExampleCsvMap>();

await foreach (var record in csvReader.GetRecordsAsync<ExampleCsv>())
{
    // Do something...
}

Additional Code:

using System.Globalization;
using System.Text;
using CsvHelper;
using CsvHelper.Configuration;
using CsvHelper.Configuration.Attributes;

namespace ExampleApp;

public sealed class ExampleCsv
{
    [Index(0), Name("ProductId")]
    public string? Id { get; set; }

    [Index(1), Name("Description Short")]
    public string? Description { get; set; }

    [Index(2, indexEnd: -1)]
    public List<int?>? TagIds { get; set; }

    public sealed class ExampleCsvMap : ClassMap<ExampleCsv>
    {
        public ExampleCsvMap()
        {
            Map(m => m.Id).Index(0).Name("ProductId");
            Map(m => m.Description).Index(1).Name("Description Short");
            Map(m => m.TagIds).Index(2, -1);
        }
    }
}

public static class CsvHlpr
{
    public static readonly Encoding DEFAULT_ENCODING_FOR_CSV_IMPORTS = Encoding.GetEncoding(1252);
    
    public static CsvReader GetCsvReader(TextReader reader, CsvConfiguration? config = null)
    {
        config ??= new CsvConfiguration(CultureInfo.InvariantCulture)
        {
            Delimiter = ";",
            TrimOptions = TrimOptions.Trim,
        };
        
        var csvReader = new CsvReader(reader, config);
        // this makes empty/whitespace strings map to null
        csvReader.Context.TypeConverterOptionsCache
            .GetOptions<string>()
            .NullValues.AddRange(["", " "]);
        
        return csvReader;
    }
}

Expected behavior
I would expect that the [Index] attribute at the property works the same as when using it via a ClassMap, but as shown above, [Index(2, indexEnd: -1)] behaves different than Map(m => m.TagIds).Index(2, -1);.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions