Create Formatted Text Files With csvhelper
This post is part of the 2018 C# advent. Be sure to check out the other great posts as well. In this post, I will show some examples for reading and writing text files using csvhelper. Csvhelper is a simple and fast library primarily used for writing csv files and implements RFC 4180. Additionally, it can be configured to use any character as a delimiter. It is available via NuGet. I have used csvhelper in a number of projects for data export features or saving preferences, etc.
I created a sample command line application to highlight reading files, writing files, and a couple advanced configuration options. The application allows the user to manage a book collection including title, author, and number of pages. I won’t be going over every line but the entire project is available at this GitHub repository. The app requires .NET Framework 4.7.1. Below is a screenshot of the main menu.
Reading files
The sample application includes a
data
folder that contains a couple of test files. Let’s import the csv file. I’m going to select option 3 and follow the prompts. Notice, one of the prompts asks for the delimiter to use. In this example, I’m going to stick to csv files but the data
folder contains a pipe ( | ) delimited file as well.

Selecting option 2 will display the list of books we just imported.

Now that we have successfully imported the books, we're going to take a closer look at the read functionality of the library. Also, notice "total pages" listed in the screenshot above is correct because page length was converted to an integer when csvhelper read the file. The full method is below. The
GetRecords
method of the CsvReader
class does the reading and parsing. The example below uses the generic version of this method but there are overloads for a type parameter or for a dynamic object. There is also a method called EnumerateRecords
. This will enumerate through the records and populate the given record instead of getting all results in one big list. Additionally, a configuration option is used to specify the character to use as a delimiter. Finally, there is a custom map class that I'll explain later in the post. The delimiter was the last input provided above and can be changed. Try running the import again and choose the Books.pipe
file and specify |
as the delimiter.
private static List<Book> Read(string filePath, string delimiter)
{
List<Book> results = new List<Book>();
try
{
using (StreamReader reader = File.OpenText(filePath))
{
CsvReader csv = new CsvReader(reader);
csv.Configuration.Delimiter = delimiter;
csv.Configuration.RegisterClassMap<BookMap>();
IEnumerable<Book> records = csv.GetRecords<Book>();
results = records.ToList();
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
return results;
}
It is also possible to only import a subset of records. To do this, we use some other methods of the
CsvReader
class, specifically, Read()
and ReadHeader()
. According to the documentation, ReadHeader()
must be called after the first call to Read()
and that allows you to loop through the records and read each one. There is also a ReadAsync()
method too. Below is the code for importing only a certain number of books. Using Read()
and ReadHeader()
we can get each row individually. It is option 4 in the demo app.
private static List Read(string filePath, string delimiter, string numberOfRecords)
{
int count = 0;
List<Book> results = new List<Book>();
if (!int.TryParse(numberOfRecords, out count))
{
Console.WriteLine("Not a valid number");
return results;
}
// Read from csv record by record up to count
try
{
using (StreamReader reader = File.OpenText(filePath))
{
CsvReader csv = new CsvReader(reader);
csv.Configuration.Delimiter = delimiter;
csv.Configuration.RegisterClassMap<BookMap>();
csv.Read();
csv.ReadHeader();
for (int i = 0; i < count; i++)
{
if (csv.Read())
{
Book record = csv.GetRecord<Book>();
results.Add(record);
}
}
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
return results;
}
Writing files
We're now going to look at writing files. First, choose option 1 in the demo app and follow the prompts to add your favorite book.

Select option 2 to see our new book added to the list

Finally, choose option 6 and follow the prompts to export the full book list.

There will be a csv file created with the exported books. The export method is below. This time we need an instance of
CsvWriter
. Like reading, writing can also specify a different delimiter. The WriteRecords
method writes all the books to the file. It is also possible to write the header and only write a certain number of records to the file. I'm not going to show that method here but the application does have an example of it with option 7.
private static void Write(List<Book> books, string filePath, string delimiter)
{
try
{
using (StreamWriter writer = File.CreateText(filePath))
{
var csv = new CsvWriter(writer);
csv.Configuration.Delimiter = delimiter;
csv.WriteRecords(books);
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
Advanced Configuration And Features
There are a number of advanced configuration options and features. Remember, when reading, we registered a map? This allows us to provide custom field mapping or custom logic like validation. The demo app contains custom validation to make sure the length of each book is greater than 0. If the validation returns false a
ValidationException
is thrown. The custom map class must inherit from the generic ClassMap
class. My implementation is below. Notice, how I call AutoMap()
first. This allows me to use the default map and then add custom validation logic to it so I only need to define the parts I want. To test this, modify the csv file so one of the records contains a length less than or equal to 0 and a ValidationException
will be thrown on import.
public sealed class BookMap : ClassMap<Book>
{
public BookMap()
{
AutoMap();
Map(m => m.Length).Validate(field =>
{
int l;
int.TryParse(field, out l);
if (l > 0)
{
return true;
}
return false;
});
}
}
Another feature of csvHelper is to sanitize fields to prevent injection attacks via a csv file. Many csv files are opened in third party applications, like Excel. If one of the fields starts with an '=' this field will be interpreted as a formula by excel and could take advantage of a vulnerability. To prevent this, any field that starts with '=', '@', '+', or '-' is prepended with a '\t' character. Through configuration options, this list can be modified, and the escape character can be changed. It's also possible to turn off sanitation all together but it is nice to know it does this out of the box. It is also possible to read individual fields. The demo application has an example of this. Option 5 will only import the titles. There are many more settings available, such as using quotes, identifying comment lines in the file, using custom type converters, and specifying different encodings, just to name a few. The csvhelper website has more documentation on what we went over here and additional features that I did not cover.
Conclusion
In conclusion, csvhelper is a feature-rich library for reading and writing, csv files, or any type of delimited text file. It is easy to get started with basic reading and writing but it is also possible to read and write parts of files or individual fields, and setup advanced configurations based on your needs. I hope this post and sample application are a good reference for the basic features of this library. The entire sample application is available at this github repository. For more information on csvhelper and where to get it, visit the csvhelper homepage. Finally, be sure to check out the rest of the posts in this year's C# Advent.