Passing Parameters To CSVHelper ClassMap
[StarLibrary, CSVHelper, C#, .NET]
Over the years, I have discussed how to use the excellent CSVHelper library for reading and writing CSV files.
We have also looked at how to control the columns and formatting of the data, as discussed in the post “Formatting Columns Using CSVHelper In C# & .NET”.
Recently, I encountered a situation where I wondered whether it was possible to use conditional logic in the ClassMap.
Let us take this example, where we have the following Person type:
public sealed record Person
{
public required string FirstName { get; init; }
public required string LastName { get; init; }
public required Gender Gender { get; init; }
}
Gender is defined thus:
public enum Gender
{
Male,
Female
}
Using the Bogus library, we generate 20 people.
var faker = new Faker<Person>()
.RuleFor(person => person.Gender, faker => faker.PickRandom<Gender>())
.RuleFor(person => person.FirstName,
(faker, person) => faker.Name.FirstName(person.Gender == Gender.Male
? Bogus.DataSets.Name.Gender.Male
: Bogus.DataSets.Name.Gender.Female))
.RuleFor(person => person.LastName, faker => faker.Name.LastName())
.UseSeed(0);
Of interest is this code:
.RuleFor(person => person.FirstName,
(faker, person) => faker.Name.FirstName(person.Gender == Gender.Male
? Bogus.DataSets.Name.Gender.Male
: Bogus.DataSets.Name.Gender.Female))
This is conditional logic to ensure that Bogus generates names appropriate to the gender, avoiding the scenario where Jane is used for a Male Person.
We then write them to a CSV, using the CSVHelper package.
First, the ClassMap:
using CsvHelper.Configuration;
public sealed class PersonClassMap : ClassMap<Person>
{
public PersonClassMap()
{
Map(m => m.LastName).Name("Last Name");
Map(m => m.FirstName).Name("First Name");
Map(m => m.Gender).Name("Gender");
}
}
Then, the code to write the CSV.
// Write csv
using var writer = new StreamWriter("People.csv");
using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);
csv.Context.RegisterClassMap<SpyClassmap>();
csv.WriteRecords(people);
The file will look like this:
| Last Name | First Name | Gender |
|---|---|---|
| Runte | Ernestine | Female |
| Macejkovic | Rita | Female |
| Wolff | Delores | Female |
| Koss | Marcus | Male |
| Wunsch | Jackie | Female |
| Ziemann | Jody | Male |
| Schumm | Veronica | Female |
| Bartoletti | Tabitha | Female |
| Watsica | Gwendolyn | Female |
| Bode | Naomi | Female |
| Gulgowski | Adrian | Male |
| Ruecker | Sandy | Female |
| Heathcote | Ricardo | Male |
| Rau | Terry | Female |
| Upton | Ricky | Male |
| Koch | Erica | Female |
| Jast | Rodney | Male |
| Kuhlman | Ada | Female |
| Shanahan | Caleb | Male |
| Parisian | Betsy | Female |
Suppose we wanted to generate TWO CSV files, with the following rules:
- Each file contains only
MaleorFemalepersons - The
FirstNamefor each file should have a header ofMaleFirstNameorFemaleFirstName, as appropriate.
An easy way to do it is to have two different ClassMaps.
For the Male CSV:
public sealed class MaleClassMap : ClassMap<Person>
{
public SpyClassmap()
{
Map(m => m.LastName).Name("Last Name");
Map(m => m.FirstName).Name("Male First Name");
Map(m => m.Gender).Name("Gender");
}
}
And the Female CSV:
public sealed class FemaleClassMap : ClassMap<Person>
{
public SpyClassmap()
{
Map(m => m.LastName).Name("Last Name");
Map(m => m.FirstName).Name("Female First Name");
Map(m => m.Gender).Name("Gender");
}
}
Can we avoid duplicating this code?
We can.
We can write a SmartClassMap that takes Gender as a constructor parameter.
public sealed class SmartPersonClassMap : ClassMap<Person>
{
public SmartPersonClassMap(Gender gender)
{
Map(m => m.LastName).Name("Last Name");
Map(m => m.FirstName).Name(gender == Gender.Male ? "Male First Name" : "Female First Name");
Map(m => m.Gender).Name("Gender");
}
}
Then we write our code to split the Persons.
First, the Male.
var males = people.Where(person => person.Gender == Gender.Male);
using var writer = new StreamWriter("Male.csv");
using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);
csv.Context.RegisterClassMap(new SmartPersonClassMap(Gender.Male));
csv.WriteRecords(males);
Next, the Female.
var females = people.Where(person => person.Gender == Gender.Female);
using var writer = new StreamWriter("Female.csv");
using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);
csv.Context.RegisterClassMap(new SmartPersonClassMap(Gender.Female));
csv.WriteRecords(females);
Our files look like this:
| Last Name | Male First Name | Gender |
|---|---|---|
| Koss | Marcus | Male |
| Ziemann | Jody | Male |
| Gulgowski | Adrian | Male |
| Heathcote | Ricardo | Male |
| Upton | Ricky | Male |
| Jast | Rodney | Male |
| Shanahan | Caleb | Male |
And:
| Last Name | Female First Name | Gender |
|---|---|---|
| Runte | Ernestine | Female |
| Macejkovic | Rita | Female |
| Wolff | Delores | Female |
| Wunsch | Jackie | Female |
| Schumm | Veronica | Female |
| Bartoletti | Tabitha | Female |
| Watsica | Gwendolyn | Female |
| Bode | Naomi | Female |
| Ruecker | Sandy | Female |
| Rau | Terry | Female |
| Koch | Erica | Female |
| Kuhlman | Ada | Female |
| Parisian | Betsy | Female |
TLDR
You can pass values to a CSVHelper ClassMap constructor to conditionally control logic.
The code is in my GitHub.
Happy hacking!