CsvHelper icon indicating copy to clipboard operation
CsvHelper copied to clipboard

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

Open IanKemp opened this issue 4 years ago • 4 comments

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

  • Add a no-args overload of ClassMap.AutoMap() that delegates to the options specified when constructing the wrapping reader/writer.
  • Add overloads of CsvContext.RegisterClassMap() that correspond to the current overloads of ClassMap.AutoMap(), accept mappers with constructors that match those overloads, and make the mapper registration invoke the appropriate constructor. For example:
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...
}

IanKemp avatar Jul 16 '21 14:07 IanKemp

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 avatar Jul 16 '21 16:07 JoshClose

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

JoshClose avatar Jul 16 '21 16:07 JoshClose

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.

IanKemp avatar Jul 19 '21 09:07 IanKemp

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

JoshClose avatar Jul 19 '21 22:07 JoshClose