Output Data to CSV

This is a class I have been using for many years to create csv files.

I like it because it is a simple class that handles both generic lists and DataTables, everything I have needed so far.

This class could be extended in many ways, but after all these years this is all that I’ve needed.

The Methods

The real benefit of the class is in the following methods

ValidateAsCSV

Excel can often confuse a CSV file for an SYLK file and will display an error message if it thinks it is.

To ensure it doesn’t get confused, this method will check the first two characters and if they are both uppercase alpha, it will put a single quote at the beginning, which seems to be the accepted workaround.

MakeValueCsvFriendly

Each value is passed through that method to make sure the values display in the CSV.

  • Dates are formatted – to my own preference
  • Quotes are handled

Using the class

In all these examples the data can be either a list of objects of a DataTable.

// export data to path using default values
 new CsvExporter().ExportToCsv(data, path);
 // export data to path without the ID column
 new CsvExporter() { ExcludeIDColumn = true }.ExportToCsv(data, path);
 // export data to path without the ID column and no headers
 new CsvExporter()
 {
     ExcludeIDColumn = true,
     IncludeHeaderLine = false
 }.ExportToCsv(data, path);

The Class

using System;
using System.Collections;
using System.Data;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;

namespace MyConsoleApp
{
    public class CsvExporter
    {
        // Include the column names as a header in the output file. Default is true
        public bool IncludeHeaderLine { get; set; } = true;
		
        // Will exclude the column ID if true. Default is false
		// May be include in data because of filtering but excluded from output
        public bool ExcludeIDColumn { get; set; } = false;
		
        // Exports a generic list to csv
		public void ExportToCsv(IList data, string savePath)
        {
            StringBuilder sb = new StringBuilder();

            Type t = data[0].GetType();
            IList propertyInfos = t.GetProperties();

            if (IncludeHeaderLine)
            {
                //add header line.
                foreach (PropertyInfo propertyInfo in propertyInfos)
                {
                    if (!(ExcludeIDColumn && propertyInfo.Name == "ID"))
                        sb.Append(propertyInfo.Name).Append(",");
                }
                sb.Remove(sb.Length - 1, 1).AppendLine();
            }

            //add value for each property.
            foreach (object obj in data)
            {
                foreach (PropertyInfo propertyInfo in propertyInfos)
                {
                    if (!(ExcludeIDColumn && propertyInfo.Name == "ID"))
                        sb.Append(MakeValueCsvFriendly(propertyInfo.GetValue(obj, null))).Append(",");
                }
                sb.Remove(sb.Length - 1, 1).AppendLine();
            }

            ValidateAsCSV(ref sb);

            File.WriteAllText(savePath, sb.ToString(), Encoding.UTF8); // this encoding works well with the degrees character
        }

        // Exports a DataTable to csv
        public void ExportToCsv(DataTable data, string savePath)
        {
            StringBuilder sb = new StringBuilder();

            if (IncludeHeaderLine)
            {
                //add header line.
                foreach (DataColumn col in data.Columns)
                {
                    if (!(ExcludeIDColumn && col.Caption == "ID"))
                        sb.Append(col.Caption).Append(",");
                }
                sb.Remove(sb.Length - 1, 1).AppendLine();
            }

            //add value for each property.
            foreach (DataRow row in data.Rows)
            {
                foreach (DataColumn col in data.Columns)
                {
                    if (!(ExcludeIDColumn && col.Caption == "ID"))
                        sb.Append(MakeValueCsvFriendly(row[col].ToString())).Append(",");
                }
                sb.Remove(sb.Length - 1, 1).AppendLine();
            }

            ValidateAsCSV(ref sb);

            File.WriteAllText(savePath, sb.ToString(), Encoding.UTF8); // this encoding works well with the degrees character
        }

        protected void ValidateAsCSV(ref StringBuilder sb)
        {
            // if the first column in a csv file is 2 upper case characters (ID) Excel thinks it's an SYLK file
            // this fix puts a single quote in front of the ID to stop being identified as SYLK
            Match m = Regex.Match(sb.ToString(), @"[A-Z][A-Z]");
            if (m.Success && m.Index == 0)
                sb.Insert(0, "'");
        }

        //get the csv value for field.
        protected string MakeValueCsvFriendly(object value)
        {
            if (value == null) return "";

            if (value is DateTime time)
            { // format date time
                if (time.TimeOfDay.TotalSeconds == 0)
                    return time.ToString("yyyy-MM-dd");
                return time.ToString("yyyy-MM-dd HH:mm:ss");
            }

            string output = value.ToString();
            if (value is string)
            {
                // look for quotes
                if (output.Contains("\""))
                {
                    // if output like "*" or ="*" and only 2 quotes in string don't wrap
                    if (!((output.Substring(0, 1) == "\"" || output.Substring(0, 2) == "=\"")
                                && output.LastIndexOf("\"") == output.Length - 1
                                && output.Count(f => f == '"') == 2))
                        output = '"' + output.Replace("\"", "\"\"") + '"';
                }

                // if contains a comma or carriage return, wrap in quotes
                if (output.Contains(",") || output.Contains("\n"))
                    output = '"' + output.Replace("\"", "\"\"") + '"';
            }
            return output;
        }
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *