How to write only selected class fields into CSV with CsvHelper? - c#

I use CsvHelper to read and write CSV files and it is great, yet I don't understand how to write only selected type fields.
Say we had:
using CsvHelper.Configuration;
namespace Project
{
public class DataView
{
[CsvField(Name = "N")]
public string ElementId { get; private set; }
[CsvField(Name = "Quantity")]
public double ResultQuantity { get; private set; }
public DataView(string id, double result)
{
ElementId = id;
ResultQuantity = result;
}
}
}
and we wanted to exclude "Quantity" CsvField from resulting CSV file that we currently generate via something like:
using (var myStream = saveFileDialog1.OpenFile())
{
using (var writer = new CsvWriter(new StreamWriter(myStream)))
{
writer.Configuration.Delimiter = '\t';
writer.WriteHeader(typeof(ResultView));
_researchResults.ForEach(writer.WriteRecord);
}
}
What could I use to dynamically exclude a type field from the CSV?
If it is necessary we could process the resulting file, yet I do not know how to remove an entire CSV column with CsvHelper.

I recently needed to achieve a similar result by determining what fields to include at runtime. This was my approach:
Create a mapping file to map which fields I need at runtime by passing in an enum into the class constructor
public sealed class MyClassMap : CsvClassMap<MyClass>
{
public MyClassMap(ClassType type)
{
switch (type)
{
case ClassType.TypeOdd
Map(m => m.Field1);
Map(m => m.Field3);
Map(m => m.Field5);
break;
case ClassType.TypeEven:
Map(m => m.Field2);
Map(m => m.Field4);
Map(m => m.Field6);
break;
case ClassType.TypeAll:
Map(m => m.Field1);
Map(m => m.Field2);
Map(m => m.Field3);
Map(m => m.Field4);
Map(m => m.Field5);
Map(m => m.Field6);
break;
}
}
}
Write out the records to using the mapping configuration
using (var memoryStream = new MemoryStream())
using (var streamWriter = new StreamWriter(memoryStream))
using (var csvWriter = new CsvWriter(streamWriter))
{
csvWriter.Configuration.RegisterClassMap(new MyClassMap(ClassType.TypeOdd));
csvWriter.WriteRecords(records);
streamWriter.Flush();
return memoryStream.ToArray();
}

Mark the field like this:
[CsvField( Ignore = true )]
public double ResultQuantity { get; private set; }
Update: Nevermind. I see you want to do this at runtime, rather than compile time. I'll leave this up as red flag for anyone else who might make the same mistake.

You can do this:
using (var myStream = saveFileDialog1.OpenFile())
{
using (var writer = new CsvWriter(new StreamWriter(myStream)))
{
writer.Configuration.AttributeMapping(typeof(DataView)); // Creates the CSV property mapping
writer.Configuration.Properties.RemoveAt(1); // Removes the property at the position 1
writer.Configuration.Delimiter = "\t";
writer.WriteHeader(typeof(DataView));
_researchResults.ForEach(writer.WriteRecord);
}
}
We are forcing the creation of the attribute mapping and then modifying it, removing the column dynamically.

I had a similar issue with my code and I fixed it by the following code.
you can do this:
var ignoreQuantity = true;
using (var myStream = saveFileDialog1.OpenFile())
{
using (var writer = new CsvWriter(new StreamWriter(myStream)))
{
var classMap = new DefaultClassMap<DataView>();
classMap.AutoMap();
classMap.Map(m => m.ResultQuantity).Ignore(ignoreQuantity)
writer.Configuration.RegisterClassMap(classMap);
writer.Configuration.Delimiter = "\t";
writer.WriteHeader(typeof(DataView));
_researchResults.ForEach(writer.WriteRecord);
}
}

I had to solve this also: I have a couple dozen record types with a common base class plus a common field that has to be ignored by all of them:
// Nothing special here
internal class MyClassMap<T> : ClassMap<T> where T : MyRecordBaseClass
{
public MyClassMap()
{
AutoMap();
Map( m => m.SOME_FIELD ).Ignore();
}
}
This part is generally well documented and not the dynamic part.
But one class needed special sauce by ignoring a different field dynamically, and though I could have created a separate map class, this didn't scale for what I expect will be a lot more of these, so I finally figured out how to do it properly:
...
// special processing for *one* record type
csvwriter.Configuration.RegisterClassMap<MyClassMap<ONE_RECORD_TYPE>>();
if (ShouldIgnore)
{
var map = csvwriter.Configuration.Maps.Find<ONE_RECORD_TYPE>();
map.Map( m => m.SOME_OTHER_FIELD ).Ignore();
}
...
This worked on CsvHelper versions 7.1.1 and 12.1.1.

Related

Not able to parse DateTime in CSVHelper

I am not able to set up proper DateTime formatting for the given csv format file. I tried different approaches but this one seems to me to be the closest to the truth. How can I set up this to make it work?
public class Parser
{
public static List<Order> ParseCsv()
{
var config = new CsvConfiguration(CultureInfo.InvariantCulture)
{
Delimiter = ";",
HasHeaderRecord = true,
TrimOptions = TrimOptions.Trim,
MissingFieldFound = null
};
using (var reader = new StringReader("'Purchase Date'\r\n'2023-02-14T12:03:40Z'"))
using (var csv = new CsvReader(reader, config))
{
csv.Context.RegisterClassMap<PurchaseMap>();
return csv.GetRecords<Order>().ToList();
}
}
}
public class PurchaseMap : ClassMap<Order>
{
public PurchaseMap()
{
Map(m => m.PurchaseDate).Name("'Purchase Date'").TypeConverterOption.Format("yyyy-MM-ddTHH:mm:ss");
}
}
public class Order
{
public DateTime PurchaseDate { get; set; }
}
The error which I got:
CsvHelper.TypeConversion.TypeConverterException: 'The conversion
cannot be performed.
Text: ''2023-02-14T12:03:40Z''
MemberName: Purchase Date
MemberType: System.DateTime
TypeConverter: 'CsvHelper.TypeConversion.DateTimeConverter'
Adding to #Jaryn's answer. You can still use TypeConverterOption.Format. Having the specific format string is the key.
public class PurchaseMap : ClassMap<Order>
{
public PurchaseMap()
{
Map(m => m.PurchaseDate).Name("'Purchase Date'").TypeConverterOption.Format("\\'yyyy-MM-ddTHH:mm:ssZ\\'");
}
}
Your CSV string is not a usual one, i.e. the values are enclosed by single quote.
Not familiar with that library, I've tried setting Escape = '\'' in CsvConfiguration but it does not work.
By eliminating single quote and change this line your code works:
Map(m => m.PurchasDate).Name("Purchas Date").TypeConverterOption.DateTimeStyles(DateTimeStyles.AdjustToUniversal);
The answer is:
Map(m => m.PurchaseDate).Convert(s =>
DateTime.ParseExact(s.Row.Parser.Record[0], "\\'yyyy-MM-ddTHH:mm:ssZ\\'", null));
Convert gives the possibility to do the parsing. Records[0] says which parsing 'column' it tries to tackle.

CsvHelper dynamic column mapping

I am following this example to map custom column names to my class model:
CsvHelper Mapping by Name
In this particular part:
public FooMap()
{
Map(m => m.Id).Name("ColumnA");
Map(m => m.Name).Name("ColumnB");
}
Is it possible to use string as column name instead of hard-code it? Something like this --
public FooMap()
{
Map("Col1").Name("ColumnA");
Map("Col2").Name("ColumnB");
}
"Col1" and "Col2" are the property of my class model. I've tried to use reflection but it didn't work:
Map(x => typeof(MyClassModel).GetProperty("Col1")).Name("ColumnA");
Please let me know if what I am trying to achieve is possible. Some additional info -- the column mapping (source and destination) are both stored in a table.
Thanks!
This should allow you to use a string to map both the property name and the header name.
void Main()
{
var mapping = new Dictionary<string, string>
{
{"Id","FooId"},
{"Name","FooName"}
};
using (var reader = new StringReader("FooId,FooName\n1,Jordan"))
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
var fooMap = new DefaultClassMap<Foo>();
fooMap.Map(mapping);
csv.Context.RegisterClassMap(fooMap);
var records = csv.GetRecords<Foo>().ToList();
}
}
public static class CsvHelperExtensions
{
public static void Map<T>(this ClassMap<T> classMap, IDictionary<string, string> csvMappings)
{
foreach (var mapping in csvMappings)
{
var property = typeof(T).GetProperty(mapping.Key);
if (property == null)
{
throw new ArgumentException($"Class {typeof(T).Name} does not have a property named {mapping.Key}");
}
classMap.Map(typeof(T), property).Name(mapping.Value);
}
}
}
public class Foo
{
public int Id { get; set; }
public string Name { get; set; }
}
As an another approach, define an XML/JSON config file where you can define the columns you want to map. Write a parser that could parse your XML/JSON config and return the columns to be mapped dynamically. This approach allows you to map any no of columns on fly, without having to recompile your code.

CsvHelper - How to map by index the whole row to a model member

I'm working on a C# windows service that reads in a csv file into a List using CsvHelper along with it's class map by index functionality. I would like to store the original raw data row in each model.
I've tried using Map(m => m.Row).Index(-1); but that did not work. I also tried ConvertUsing, but I get a message that MemberMap does not contain a definition for 'ConvertUsing'.
The RegisterClassMap and csv.GetRecords functionality is doing a bulk read that doesn't give me an opportunity to capture the original raw data row.
Any help would be greatly appreciated. I need to create an email with the status (sending the data to a micro service) and the original raw data two, and would love to store it while CsvHelper is reading the file.
void Main()
{
var s = new StringBuilder();
s.Append("Id,Name\r\n");
s.Append("1,one\r\n");
var config = new CsvConfiguration(CultureInfo.InvariantCulture)
{
};
using (var reader = new StringReader(s.ToString()))
using (var csv = new CsvReader(reader, config))
{
csv.Context.RegisterClassMap<FooMap>();
csv.GetRecords<Foo>().ToList().Dump();
}
}
private class Foo
{
public int Id { get; set; }
public string Name { get; set; }
public string RawRow { get; set; }
}
private class FooMap : ClassMap<Foo>
{
public FooMap()
{
Map(m => m.Id);
Map(m => m.Name);
Map(m => m.RawRow).Convert(args => args.Row.Parser.RawRecord);
}
}
.Dump() is a LINQPad method.

CsvHelper - ClassMap by index for file with no header errors on read

I'm using csvhelper 18.0 and am trying to update some code that used 2.8.4.
I have a file that I'm trying to read that has no headers.
I've defined a ClassMap to map by index.
I've created the configuration of the CsvReader so HasHeaderRecord = false.
When I try to import this file, I get an error that states There is no header record to determine the index by name. I'm confused as to why an error is being thrown regarding the header record. The header record does not exist, which is why I am mapping with an index.
Would anyone know how I can read a headerless file and still map to a class?
Here is the class and mapping class:
public class TFile
{
public int Wn { get; set; }
public string Hiwn { get; set; }
public string Sync { get; set; }
}
public sealed class TFileMap : ClassMap<TFile>
{
public TFileMap()
{
Map(m => m.Wn).Index(0);
Map(m => m.Hiwn).Index(1);
Map(m => m.Sync).Index(2);
}
}
Here is the piece of code that throughs the error:
using (TextReader textReader = new StringReader(data))
{
var csvT = new CsvReader(textReader, CultureInfo.InvariantCulture);
csvT.Configuration.HasHeaderRecord = false;
csvT.Configuration.RegisterClassMap<TFileMap>();
csvT.Configuration.CultureInfo = new CultureInfo("en-AU");
// error occurs on this line
tData1 = csvT.GetRecords<TFile>().ToList();
}
Here is a small sample file:
37,1R,Y
38,1L,Y
39,2R,Y
40,2L,Y
Any help would be greatly appreciated. Thanks
I pulled CsvHelper version 18.0 and tried with your code and sample data and it worked just fine for me.
var data = #"37,1R,Y
38,1L,Y
39,2R,Y
40,2L,Y";
using (TextReader textReader = new StringReader(data))
{
var csvT = new CsvReader(textReader, CultureInfo.InvariantCulture);
csvT.Configuration.HasHeaderRecord = false;
csvT.Configuration.RegisterClassMap<TFileMap>();
csvT.Configuration.CultureInfo = new CultureInfo("en-AU");
var tData1 = csvT.GetRecords<TFile>().ToList();
tData1.Dump();
}

CsvHelper and immutable types

I have an immutable class that I want to write to and read from a CSV file. The issue is I am getting an exception when reading the CSV despite having mapped the object and set up a configuration that should allow this to work.
To do this I am using CsvHelper. The immutable class looks like the following.
public class ImmutableTest
{
public Guid Id { get; }
public string Name { get; }
public ImmutableTest(string name) : this(Guid.NewGuid(), name)
{
}
public ImmutableTest(Guid id, string name)
{
Id = id;
Name = name;
}
}
I have no issue writing this to a CSV file, but when I try to read it from a file, I get the following exception.
No members are mapped for type 'CsvTest.Program+ImmutableTest'
However, I have mapped the members for this class in the map class below.
public sealed class ImmutableTestMap : ClassMap<ImmutableTest>
{
public ImmutableTestMap()
{
Map(immutableTest => immutableTest.Id)
.Index(0)
.Name(nameof(ImmutableTest.Id).ToUpper());
Map(immutableTest => immutableTest.Name)
.Index(1)
.Name(nameof(ImmutableTest.Name));
}
}
I have also tried to configure the reader to use the constructor to build the object by using the following configuration.
Configuration config = new Configuration
{
IgnoreBlankLines = true
};
config.RegisterClassMap<ImmutableTestMap>();
config.ShouldUseConstructorParameters = type => true;
config.GetConstructor = type => type.GetConstructors()
.MaxBy(constructor => constructor.GetParameters().Length)
.FirstOrDefault();
No of this seems to be working. Where am I going wrong?
Complete MCVE .NET Framework Console Example
Install the packages
Install-Package CsvHelper
Install-Package morelinq
Sample console program
using System;
using System.IO;
using CsvHelper;
using CsvHelper.Configuration;
using MoreLinq;
namespace CsvTest
{
class Program
{
static void Main()
{
Configuration config = new Configuration
{
IgnoreBlankLines = true
};
config.RegisterClassMap<ImmutableTestMap>();
config.ShouldUseConstructorParameters = type => true;
config.GetConstructor = type => type.GetConstructors()
.MaxBy(constructor => constructor.GetParameters().Length)
.FirstOrDefault();
const string filePath = "Test.csv";
using (FileStream file = new FileStream(filePath, FileMode.Create))
using (StreamWriter fileWriter = new StreamWriter(file))
using (CsvSerializer csvSerializer = new CsvSerializer(fileWriter, config))
using (CsvWriter csvWriter = new CsvWriter(csvSerializer))
{
csvWriter.WriteHeader<ImmutableTest>();
csvWriter.NextRecord();
csvWriter.WriteRecord(new ImmutableTest("Test 1"));
csvWriter.NextRecord();
csvWriter.WriteRecord(new ImmutableTest("Test 2"));
csvWriter.NextRecord();
}
using (FileStream file = new FileStream(filePath, FileMode.Open))
using (StreamReader fileReader = new StreamReader(file))
using (CsvReader csvReader = new CsvReader(fileReader, config))
{
foreach (ImmutableTest record in csvReader.GetRecords<ImmutableTest>())
{
Console.WriteLine(record.Id);
Console.WriteLine(record.Name);
Console.WriteLine();
}
}
}
public sealed class ImmutableTestMap : ClassMap<ImmutableTest>
{
public ImmutableTestMap()
{
Map(immutableTest => immutableTest.Id)
.Index(0)
.Name(nameof(ImmutableTest.Id).ToUpper());
Map(immutableTest => immutableTest.Name)
.Index(1)
.Name(nameof(ImmutableTest.Name));
}
}
public class ImmutableTest
{
public Guid Id { get; }
public string Name { get; }
public ImmutableTest(string name) : this(Guid.NewGuid(), name)
{
}
public ImmutableTest(Guid id, string name)
{
Id = id;
Name = name;
}
}
}
}
If the type is immutable, it will use constructor mapping instead. Your constructor variable names need to match the header names. You can do this using Configuration.PrepareHeaderForMatch.
void Main()
{
var s = new StringBuilder();
s.AppendLine("Id,Name");
s.AppendLine($"{Guid.NewGuid()},one");
using (var reader = new StringReader(s.ToString()))
using (var csv = new CsvReader(reader))
{
csv.Configuration.PrepareHeaderForMatch = (header, indexer) => header.ToLower();
csv.GetRecords<ImmutableTest>().ToList().Dump();
}
}
public class ImmutableTest
{
public Guid Id { get; }
public string Name { get; }
public ImmutableTest(string name) : this(Guid.NewGuid(), name)
{
}
public ImmutableTest(Guid id, string name)
{
Id = id;
Name = name;
}
}
So the issue here is that there are no ParameterMaps in the class map.
The easy way to fix this, with the example above, is to write something like
public ImmutableTestMap()
{
AutoMap();
Map(immutableTest => immutableTest.Id)
.Index(0)
.Name(nameof(ImmutableTest.Id).ToUpper());
}
But this causes an issue that the column headers in the CSV file are ID, Name and the parameter maps generated are id, name. These are not equal to each other and so the reader throws an error saying it can't find a column with the name ID. It also says you can set header validated to null. After playing around with this, I ended up at a point where the configuration
Configuration config = new Configuration
{
IgnoreBlankLines = true,
ShouldUseConstructorParameters = type => true,
GetConstructor = type => type.GetConstructors()
.MaxBy(constructor => constructor.GetParameters().Length)
.FirstOrDefault(),
HeaderValidated = null,
MissingFieldFound = null
};
config.RegisterClassMap<ImmutableTestMap>();
Is trying to parse empty fields and failing to convert it to a GUID. So it looked like that road was dead.
To get around this, I looked at checking each parameter map generated by auto map for the "id" value and then to replace it with "ID". However, this also did not work as the auto generated maps are generated with a null name. The name is only assigned when SetMapDefaults are called during the registration of a map in a config.
So I went for a blind loop of setting the names of the parameters maps to the explicitly defined member maps.
public ImmutableTestMap()
{
AutoMap();
Map(immutableTest => immutableTest.Id)
.Index(0)
.Name(nameof(ImmutableTest.Id).ToUpper());
Map(immutableTest => immutableTest.Name)
.Index(0)
.Name(nameof(ImmutableTest.Id));
if (MemberMaps.Count != ParameterMaps.Count)
{
return;
}
for (int i = 0; i < MemberMaps.Count; i++)
{
ParameterMaps[i].Data.Name = MemberMaps[i].Data.Names[0];
}
}
However, I would say this is more of a bodge than a fix and would definitely be open to other answers.

Categories

Resources