JoshClose / CsvHelper

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

Provide a function to transform property names to header names #2187

Open CruseCtrl opened 10 months ago

CruseCtrl commented 10 months ago

Is your feature request related to a problem? Please describe. When writing a csv, I want most of my header names to be a transformed version of my property names, e.g. in camelCase, or "Title Case", but I don't want to have to type these all out manually in Name attributes or in a class map

Describe the solution you'd like It would be nice if I could do something like

var config = new CsvConfiguration(CultureInfo.InvariantCulture)
{
    GetDefaultHeaderName = propertyName => GetHeaderName(propertyName),
};

and then I could write my own GetHeaderName function which would take e.g. MyPropertyName and convert it into myPropertyName or My Property Name. Then I wouldn't have to have a [Name("My Property Name")] attribute or use a class map

Describe alternatives you've considered This works, but is a bit long-winded and requires me to know a bit more about how CsvHelper uses its maps:

var map = new DefaultClassMap<SalesMasterExportRow>();
map.AutoMap(csv.Configuration.CultureInfo);
foreach (var memberMap in map.MemberMaps)
{
    memberMap.Data.Names.Add(GetHeaderName(memberMap.Data.Member?.Name));
}

csv.Context.RegisterClassMap(map);

Additional context It looks like the default name of a member gets set in ClassMapCollection.SetMapDefaults:

if (memberMap.Data.Names.Count == 0)
{
    memberMap.Data.Names.Add(memberMap.Data.Member.Name);
}

This could be changed to

if (memberMap.Data.Names.Count == 0)
{
    memberMap.Data.Names.Add(context.Configuration.GetDefaultHeaderName(memberMap.Data.Member.Name));
}

I'm happy to raise a pull request if this is something that's likely to get accepted? I'm not sure whether GetDefaultHeaderName is the best name for it though, so I'm open to other suggestions

Rob-Hague commented 10 months ago

Have you looked at CsvConfiguration.PrepareHeaderForMatch? e.g.

void Main()
{
    string csvString = """
    My Property
    value
    """;

    CsvConfiguration config = new(CultureInfo.InvariantCulture)
    {
        PrepareHeaderForMatch = args => args.Header.Replace(" ", "")
    };

    using StringReader sr = new(csvString);
    using CsvReader csv = new(sr, config);
    var records = csv.GetRecords<MyClass>().ToList();
}

class MyClass
{
    public string MyProperty { get; set; }
}
CruseCtrl commented 10 months ago

@Rob-Hague thanks. I had tried that, but it only seems to be used when reading a file and is ignored when writing to a file

CruseCtrl commented 10 months ago

I guess another solution would be to make PrepareHeaderForMatch affect writing files as well, but that would be a breaking change to current behaviour so is a bit more risky

Rob-Hague commented 10 months ago

Yeah that would be a bit strange. Personally I think your solution is totally valid, but I would probably tweak it slightly:

var map = csv.Context.AutoMap<SalesMasterExportRow>(); // this ensures the context is passed when mapping

foreach (var memberMap in map.MemberMaps)
{
    memberMap.Data.Names.Clear();
    memberMap.Data.Names.Add(GetHeaderName(memberMap.Data.Member?.Name));
}

// This is no longer necessary
// csv.Context.RegisterClassMap(map);

But that would override any existing name mapping which may not be desired. I think your configuration idea would work as well. I am not a maintainer so can't comment on whether it would be accepted.

CruseCtrl commented 10 months ago

The problem is that I still want to be able to override the default by using a [Name] attribute. Most of my fields have a predictable mapping, but not all of them.

I see that there are already 19 open PRs, so I'm a bit worried that any work I do will just get ignored and be a waste of time. The last time a PR was merged was December last year :(