JoshClose / CsvHelper

Library to help reading and writing CSV files
http://joshclose.github.io/CsvHelper/
Other
4.78k stars 1.07k forks source link

ClassMap.AutoMap requires parameters, but does not allow these to be populated from derived class mappers #1828

Open IanKemp opened 3 years ago

IanKemp commented 3 years ago

Is your feature request related to a problem? Please describe. I have the following class and map defined:

public abstract class CsvBaseModel
{
    public long LineNumber { get; set; }
}

public class CsvBaseModelClassMap<TCsvBaseModel> : ClassMap<TCsvBaseModel>
    where TCsvBaseModel : CsvBaseModel
{
    public CsvBaseModelClassMap()
    {
        AutoMap(CultureInfo.InvariantCulture); // problem here
        Map(m => m.LineNumber).Convert(record => record.Row.Parser.RawRow);
    }
}

which I'm registering as follows:

using (var stream = ...)
using (var reader = new StreamReader(stream))
using (var csv = new CsvReader(reader, culture))
{
    csv.Context.RegisterClassMap<CsvBaseModelClassMap<DerivedFromCsvBaseModel>>();

    ... do reading here...
}

Essentially I'm using the ClassMap.AutoMap() method to convention-based map all the properties introduced by the derived classes, as well as add the line number.

The problem is that AutoMap() has 3 overloads, all of which have mandatory parameter(s), but CsvContext.RegisterClassMap() doesn't allow those parameters to be specified, so there's no way to control what is passed to AutoMap(). Based on my testing, it does appear that whatever is passed to these overloads is ignored in favour of the options specified for the CsvReader when constructing it, but even so this is confusing.

Describe the solution you'd like

public class CsvBaseModelClassMap<TCsvBaseModel> : ClassMap<TCsvBaseModel>
    where TCsvBaseModel : CsvBaseModel
{
    public CsvBaseModelClassMap(CultureInfo cultureInfo)
    {
        AutoMap(cultureInfo);
        Map(m => m.LineNumber).Convert(record => record.Row.Parser.RawRow);
    }
}

...

using (var stream = ...)
using (var reader = new StreamReader(stream))
using (var csv = new CsvReader(reader, culture))
{
    // note that this version of RegisterClassMap and the CsvBaseModelClassMap constructor both accept a CultureInfo.
    // when the actual reading and mapping take place, this specific CultureInfo provided to RegisterClassMap will be
    // passed into CsvBaseModelClassMap's constructor and used, instead of the one specified in the CsvReader constructor.
    csv.Context.RegisterClassMap<CsvBaseModelClassMap<DerivedFromCsvBaseModel>>(
        CultureInfo.GetCultureInfo("whatever-WHATEVER")
    );

    ... do reading here...
}
JoshClose commented 3 years ago

RegisterClassMap has an overload that takes in a map, so you can do this:

var map = new CsvBaseModelClassMap<DerivedFromCsvBaseModel>(CultureInfo.GetCultureInfo("whatever-WHATEVER"));
csv.Context.RegisterClassMap(map);
JoshClose commented 3 years ago

https://github.com/JoshClose/CsvHelper/blob/808dea2456b9c695eed1c124f67a2385e88b8a81/src/CsvHelper/CsvContext.cs#L136

IanKemp commented 3 years ago

Hrm, that kinda works, except I don't really want to have to manually instantiate the ClassMap. I much prefer convention-based mapping, hence this proposal... perhaps I should reword it.

JoshClose commented 3 years ago

Is there a reason you can't call new? Like do you only know the type at runtime?