Object to CSV extension

Here is a nice little extension that will take an object and convert it into a CSV file.

Also has the option if you want to include a header on the CSV file.

public static class ObjectToCsv
 {
 public static string ToCsv<T>(this IEnumerable<T> items, bool includeHeading = true)
 where T : class
 {
 var csvBuilder = new StringBuilder();
 var properties = typeof(T).GetProperties();

 if (includeHeading)
 {
 csvBuilder.AppendLine(string.Join(",", properties.Select(p => p.Name.ToCsvValue()).ToArray()));
 }

 foreach (var item in items)
 {
 var line = string.Join(",", properties.Select(p => p.GetValue(item, null).ToCsvValue()).ToArray());
 csvBuilder.AppendLine(line);
 }
 return csvBuilder.ToString();
 }

 private static string ToCsvValue<T>(this T item)
 {
 if (item == null) return "\"\"";

 if (item is string)
 {
 return $"\"{item.ToString().Replace("\"", "\\\"")}\"";
 }
 double dummy;
 return double.TryParse(item.ToString(), out dummy) ? $"{item}" : $"\"{item}\"";
 }
 }

CSV file parser and writer in C# (Part 3)

This is the last part of an article series exploring reading and writing CSV files with C#/.NET.

Part 1 covered converting the contents of a DataTable into CSV format. Part 2 explained how to read a CSV file back into a DataTable.

Finally we will look at some test cases for use with NUnit, a very important tool that I'll describe in more detail in an upcoming article. There is quite a bit of code, but it is all very simple and easy to understand. Each method marked with [Test] will be called by NUnit and run one of the CSV related methods in turn. Assert statements will then check if the returned values are valid or not.

First, let's check the parser methods. There are several tests here that all test something different; they all pass a CSV formatted string to the CsvParser.Parse method and check the returned DataTable.

using System;
using System.Data;
using System.IO;
using NUnit.Framework;

namespace CsvParser
{
        [TestFixture]
        public class TestCsvParser
        {
                [Test]
                public void UnquotedLine()
                {
                        DataTable table = CsvParser.Parse("one,two,three");
                        Assert.IsNotNull(table);
                        Assert.AreEqual(3, table.Columns.Count);
                        Assert.AreEqual(1, table.Rows.Count);
                        Assert.AreEqual("one", table.Rows[0][0]);
                        Assert.AreEqual("two", table.Rows[0][1]);
                        Assert.AreEqual("three", table.Rows[0][2]);
                }

                [Test]
                public void QuotedLine()
                {
                        DataTable table = CsvParser.Parse("\"one\",\"two\",\"three\"");
                        Assert.IsNotNull(table);
                        Assert.AreEqual(3, table.Columns.Count);
                        Assert.AreEqual(1, table.Rows.Count);
                        Assert.AreEqual("one", table.Rows[0][0]);
                        Assert.AreEqual("two", table.Rows[0][1]);
                        Assert.AreEqual("three", table.Rows[0][2]);
                }

                [Test]
                public void TwoLines()
                {
                        DataTable table = CsvParser.Parse("one,two,three\nfour,five,six\n");
                        Assert.IsNotNull(table);
                        Assert.AreEqual(3, table.Columns.Count);
                        Assert.AreEqual(2, table.Rows.Count);
                        Assert.AreEqual("one", table.Rows[0][0]);
                        Assert.AreEqual("two", table.Rows[0][1]);
                        Assert.AreEqual("three", table.Rows[0][2]);
                        Assert.AreEqual("four", table.Rows[1][0]);
                        Assert.AreEqual("five", table.Rows[1][1]);
                        Assert.AreEqual("six", table.Rows[1][2]);
                }

                [Test]
                public void QuotedMultilineValue()
                {
                        DataTable table = CsvParser.Parse(
                                "\"one\n\"\"beer\"\"\",\"\"\"two\"\"\nbeers\",\"three\nbeers\"");
                        Assert.IsNotNull(table);
                        Assert.AreEqual(3, table.Columns.Count);
                        Assert.AreEqual(1, table.Rows.Count);
                        Assert.AreEqual("one\n\"beer\"", table.Rows[0][0]);
                        Assert.AreEqual("\"two\"\nbeers", table.Rows[0][1]);
                        Assert.AreEqual("three\nbeers", table.Rows[0][2]);
                }

                [Test]
                public void Headers()
                {
                        DataTable table = CsvParser.Parse(
                                "First,Last,Email\nAndreas,Knab,knabar@yahoo.com", true);
                        Assert.IsNotNull(table);
                        Assert.AreEqual(3, table.Columns.Count);
                        Assert.AreEqual(1, table.Rows.Count);
                        Assert.AreEqual("Andreas", table.Rows[0]["First"]);
                        Assert.AreEqual("Knab", table.Rows[0]["Last"]);
                        Assert.AreEqual("knabar@yahoo.com", table.Rows[0]["Email"]);
                }

                [Test]
                public void TrailingNewlines()
                {
                        DataTable table = CsvParser.Parse("test\n\n\n\n\n\n");
                        Assert.IsNotNull(table);
                        Assert.AreEqual(1, table.Columns.Count);
                        Assert.AreEqual(6, table.Rows.Count);
                        Assert.AreEqual("test", table.Rows[0][0]);
                }

                [Test]
                public void Newlines()
                {
                        DataTable table = CsvParser.Parse("test1\x0Atest2\x0Dtest3\x0D\x0Atest4");
                        Assert.IsNotNull(table);
                        Assert.AreEqual(1, table.Columns.Count);
                        Assert.AreEqual(4, table.Rows.Count);
                        Assert.AreEqual("test1", table.Rows[0][0]);
                        Assert.AreEqual("test2", table.Rows[1][0]);
                        Assert.AreEqual("test3", table.Rows[2][0]);
                        Assert.AreEqual("test4", table.Rows[3][0]);
                }
        }
}

Second, the opposite route – testing the CsvWriter. Since it takes quite a bit of effort to completely populate a DataTable, I'll cheat and create the DataTable using the CsvParser, which has been tested separately and is known to work.

using System;
using System.Data;
using System.IO;
using NUnit.Framework;
using CsvParser;

namespace CsvWriter
{
        [TestFixture]
        public class TestCsvWriter
        {
                [Test]
                public void PlainText()
                {
                        string s = "one,two,three\n";
                        DataTable table = CsvParser.Parse(s);
                        string t = CsvWriter.WriteToString(table, false, false);
                        Assert.AreEqual(s, t);
                }

                [Test]
                public void QuotedText()
                {
                        string s = "\"one\",\"two\",\"three\"\n";
                        DataTable table = CsvParser.Parse(s);
                        string t = CsvWriter.WriteToString(table, false, true);
                        Assert.AreEqual(s, t);
                }

                [Test]
                public void MultiLineText()
                {
                        string s = "\"one\nline\",\"two\nline\",\"three\nline\"\n";
                        DataTable table = CsvParser.Parse(s);
                        string t = CsvWriter.WriteToString(table, false, false);
                        Assert.AreEqual(s, t);
                }

                [Test]
                public void Headers()
                {
                        string s = "First,Last,Email\nAndreas,Knab,knabar@yahoo.com\n";
                        DataTable table = CsvParser.Parse(s, true);
                        string t = CsvWriter.WriteToString(table, true, false);
                        Assert.AreEqual(s, t);
                }
        }
}

And that's it! There are more special cases that could be tested, especially invalid input like non-CSV files, but technically everything can be interpreted as CSV, so the question would be what output to expect.

As I mentioned earlier, NUnit is worth a closer look, so check back for more.

CSV file parser and writer in C# (Part 2)

This is the second part of an article series exploring reading and writing CSV files with C#/.NET.

Part 1 covered converting the contents of a DataTable into CSV format; this part explains reading a CSV file back into a DataTable.

First some namespace imports and the namespace declaration for this project:

using System;
using System.Collections;
using System.Data;
using System.Text;
using System.IO;

namespace CsvParser
{

Like the CsvWriter class, all methods in the CsvParser class are static. There are four parser methods that return a DataTable from either a string or text stream, and expecting a header line in the CSV source or not. Only one method has a real body, all others just adjust their parameter signature. The implementation is simple: the method reads one "row" of the CSV source at a time and populates a row in the DataTable. The real meat is the private class CsvStream explained below. There is one utility method that returns an unused column name for a DataTable, in case there are no headers in the CSV source or the headers are not unique.

        public class CsvParser
        {
                public static DataTable Parse(string data, bool headers)
                {
                        return Parse(new StringReader(data), headers);
                }
               
                public static DataTable Parse(string data)
                {
                        return Parse(new StringReader(data));
                }

                public static DataTable Parse(TextReader stream)
                {
                        return Parse(stream, false);
                }

                public static DataTable Parse(TextReader stream, bool headers)
                {
                        DataTable table = new DataTable();
                        CsvStream csv = new CsvStream(stream);
                        string[] row = csv.GetNextRow();
                        if (row == null)
                                return null;
                        if (headers)
                        {
                                foreach (string header in row)
                                {
                                        if (header != null && header.Length > 0 && !table.Columns.Contains(header))
                                                table.Columns.Add(header, typeof(string));
                                        else
                                                table.Columns.Add(GetNextColumnHeader(table), typeof(string));
                                }
                                row = csv.GetNextRow();
                        }
                        while (row != null)
                        {
                                while (row.Length > table.Columns.Count)
                                        table.Columns.Add(GetNextColumnHeader(table), typeof(string));
                                table.Rows.Add(row);
                                row = csv.GetNextRow();
                        }
                        return table;
                }

                private static string GetNextColumnHeader(DataTable table)
                {
                        int c = 1;
                        while (true)
                        {
                                string h = "Column" + c++;
                                if (!table.Columns.Contains(h))
                                        return h;
                        }
                }

The CsvStream class does the actual work – read the CSV source in one character at a time and return meaningful chunks of decoded data, namely data items and rows.

                private class CsvStream
                {
                        private TextReader stream;                 
                       
                        public CsvStream(TextReader s)
                        {
                                stream = s;
                        }

                        public string[] GetNextRow()
                        {
                                ArrayList row = new ArrayList();
                                while (true)
                                {
                                        string item = GetNextItem();
                                        if (item == null)
                                                return row.Count == 0 ? null : (string[])row.ToArray(typeof(string));
                                        row.Add(item);
                                }
                        }

                        private bool EOS = false;
                        private bool EOL = false;

                        private string GetNextItem()
                        {
                                if (EOL)
                                {
                                        // previous item was last in line, start new line
                                        EOL = false;
                                        return null;
                                }

                                bool quoted = false;
                                bool predata = true;
                                bool postdata = false;
                                StringBuilder item = new StringBuilder();
                               
                                while (true)
                                {
                                        char c = GetNextChar(true);
                                        if (EOS)
                                                return item.Length > 0 ? item.ToString() : null;

                                        if ((postdata || !quoted) && c == ',')
                                                // end of item, return
                                                return item.ToString();
                                       
                                        if ((predata || postdata || !quoted) && (c == '\x0A' || c == '\x0D'))
                                        {
                                                // we are at the end of the line, eat newline characters and exit
                                                EOL = true;
                                                if (c == '\x0D' && GetNextChar(false) == '\x0A')
                                                        // new line sequence is 0D0A
                                                        GetNextChar(true);
                                                return item.ToString();
                                        }

                                        if (predata && c == ' ')
                                                // whitespace preceeding data, discard
                                                continue;

                                        if (predata && c == '"')
                                        {
                                                // quoted data is starting
                                                quoted = true;
                                                predata = false;
                                                continue;
                                        }

                                        if (predata)
                                        {
                                                // data is starting without quotes
                                                predata = false;
                                                item.Append(c);
                                                continue;
                                        }

                                        if (c == '"' && quoted)
                                        {
                                                if (GetNextChar(false) == '"')
                                                        // double quotes within quoted string means add a quote       
                                                        item.Append(GetNextChar(true));
                                                else
                                                        // end-quote reached
                                                        postdata = true;
                                                continue;
                                        }

                                        // all cases covered, character must be data
                                        item.Append(c);
                                }
                        }

                        private char[] buffer = new char[4096];
                        private int pos = 0;
                        private int length = 0;

                        private char GetNextChar(bool eat)
                        {
                                if (pos >= length)
                                {
                                        length = stream.ReadBlock(buffer, 0, buffer.Length);
                                        if (length == 0)
                                        {
                                                EOS = true;
                                                return '\0';
                                        }
                                        pos = 0;
                                }
                                if (eat)
                                        return buffer[pos++];
                                else
                                        return buffer[pos];
                        }
                }
        }
}

And that's about it. In an upcoming part of this article I'll share some NUnit test cases for these classes.

CSV file parser and writer in C# (Part 1)

An issue that comes up quite frequently is how to read and write comma seperated value (CSV) files in C#. Surprisingly the .NET libraries have no built-in support for this, and the usual solution to use an OleDb connection to the CSV file with Microsoft Excel's database driver is convoluted and not cross-platform.

CSV files have a very simple structure (source):

  • Each record is one line (with exceptions)
  • Fields are separated with commas
  • Leading and trailing space-characters adjacent to comma field separators are ignored
  • Fields with embedded commas must be delimited with double-quote characters
  • Fields that contain double quote characters must be surounded by double-quotes, and the embedded double-quotes must each be represented by a pair of consecutive double quotes.
  • A field that contains embedded line-breaks must be surounded by double-quotes
  • Fields with leading or trailing spaces must be delimited with double-quote characters
  • Fields may always be delimited with double quotes
  • The first record in a CSV file may be a header record containing column (field) names

In this article, I'll provide simple, but fully functional code to read and write a CSV file according to these rules. In memory, data will be represented as a DataTable, which makes it easy to process; for storage in the file system or transfer over a network the CSV data will be stored as a String or in a Stream.


First, the easy part: writing a DataTable to a CSV file.

public class CsvWriter
{
        public static string WriteToString(DataTable table, bool header, bool quoteall)
        {
                StringWriter writer = new StringWriter();
                WriteToStream(writer, table, header, quoteall);
                return writer.ToString();
        }

        public static void WriteToStream(TextWriter stream, DataTable table, bool header, bool quoteall)
        {
                if (header)
                {
                        for (int i = 0; i < table.Columns.Count; i++)
                        {
                                WriteItem(stream, table.Columns[i].Caption, quoteall);
                                if (i < table.Columns.Count1)
                                        stream.Write(',');
                                else
                                        stream.Write('\n');
                        }
                }
                foreach (DataRow row in table.Rows)
                {
                        for (int i = 0; i < table.Columns.Count; i++)
                        {
                                WriteItem(stream, row[i], quoteall);
                                if (i < table.Columns.Count1)
                                        stream.Write(',');
                                else
                                        stream.Write('\n');
                        }
                }
        }

        private static void WriteItem(TextWriter stream, object item, bool quoteall)
        {
                if (item == null)
                        return;
                string s = item.ToString();
                if (quoteall || s.IndexOfAny("\",\x0A\x0D".ToCharArray()) > –1)
                        stream.Write("\"" + s.Replace("\"", "\"\"") + "\"");
                else
                        stream.Write(s);
        }
}

The methods are static since the whole conversion is done in one method call, there is no need to create object instances etc.

WriteToString will return the CSV file in a string; it is just a wrapper around the more generic WriteToStream method. Both methods take a DataTable and two boolean flags to indicate if you want to write a header line (which would use the column headers of the DataTable) and if you want to quote all values instead of only values that need to be quoted.

Since CSV files do not work well for binary data, your DataTable should not contain any, although the resulting file would still be valid and could be read back in.

WriteToStream just loops through all rows and columns of the DataTable and writes the individual data items to the output stream.

The WriteItem method finally encodes an individual data item and, if necessary or requested, adds quotes around it.

In upcoming parts of this article series, I'll provide and explain code to read a CSV file back into a DataTable, and how to use NUnit to test everything.