CsvHelper icon indicating copy to clipboard operation
CsvHelper copied to clipboard

Provide a function to transform property names to header names

Open CruseCtrl opened this issue 1 year ago • 5 comments

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

CruseCtrl avatar Sep 11 '23 13:09 CruseCtrl

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; }
}

Rob-Hague avatar Sep 12 '23 08:09 Rob-Hague

@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 avatar Sep 12 '23 09:09 CruseCtrl

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

CruseCtrl avatar Sep 12 '23 14:09 CruseCtrl

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.

Rob-Hague avatar Sep 12 '23 15:09 Rob-Hague

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 :(

CruseCtrl avatar Sep 12 '23 16:09 CruseCtrl