CsvHelper
CsvHelper copied to clipboard
Provide a function to transform property names to header names
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
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 thanks. I had tried that, but it only seems to be used when reading a file and is ignored when writing to a file
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
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.
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 :(