IEnumerable ToCsv() extension method

In a recently concluded project that had many tabular reports, I had to display a link on each report to download the data as a comma-delimited file. Additionally, the downloaded file was expected to have a header row to help identify the columns when imported into excel.

All the reports accepted an IEnumerable<T> as data source and all the properties of the model class were decorated with the JsonProperty attribute from the Json.net library.

public class Car  
{
    [JsonProperty(PropertyName = "make", Order = 1)]
    public string Make { get; set; }

    [JsonProperty(PropertyName = "year", Order = 0)]
    public int Year { get; set; }

    [JsonProperty(PropertyName = "price", Order = 2)]
    public decimal Price { get; set; }
}

I decided to write a helper method to generate a comma-delimted string from an IEnumerable leveraging the JsonProperty attribute for header column name. This is how it turned out to be.

namespace App.Web.Helpers  
{
    public class Helper
    {
        public static void WriteCSV(
               IEnumerable data,
               TextWriter output,
               bool headerRow = false)
        {
            if (data == null)
            {
                throw new ArgumentNullException("data");
            }

            const char separator = ',';
            Type type = null;

            if (data.GetType().IsGenericType)
            {
                type = data.GetType().GetGenericArguments()[0];
            }
            else
            {
                // get the type of the data item
                // perhaps there is a better way
                foreach (var item in data)
                {
                    type = item.GetType();
                    break;
                }
            }

            // only take properties with [JsonProperty] attribute defined
            var propsInfo = type.GetProperties(BindingFlags.Public 
                                    | BindingFlags.Instance)
                          .Where(p => p.IsDefined(typeof(JsonPropertyAttribute), true));

            if (!propsInfo.Any())
            {
                return;
            }

            // create a collection of properties to write
            var propsToWrite = new List<PropertyInfoWrapper>();

            foreach (var pi in propsInfo)
            {
                // get the [JsonProperty] attribute
                var attr = (JsonPropertyAttribute)Attribute
                     .GetCustomAttribute(pi, typeof(JsonPropertyAttribute));

                propsToWrite.Add(new PropertyInfoWrapper(pi) { Order = attr.Order });

                // write header columns
                if (headerRow)
                {
                    // use the actual property name if PropertyName attribute is null
                    string displayName = attr.PropertyName ?? pi.Name;

                    // if the display name contains a separator char
                    // enclose it in double quotes
                    if (displayName.IndexOf(separator) != -1)
                    {
                        output.Write("\"" + displayName + "\"");
                    }
                    else
                    {
                        output.Write(displayName);
                    }

                    output.Write(separator);
                }
            }

            if (headerRow)
            {
                output.WriteLine();
            }

            bool firstProperty = false;
            // sort properties by the Order property then by Property name
            var sortedProps = propsToWrite
                      .OrderBy(p => p.Order)
                      .ThenBy(p => p.PropertyInfo.Name));                

            // write the data items
            foreach (var item in data)
            {
                firstProperty = true;                
                foreach (var prop in sortedProps)
                {
                    // prevents a leading separator char on a row
                    if (!firstProperty)
                    {
                        output.Write(separator);
                    }
                    firstProperty = false;

                    var pi = prop.PropertyInfo;

                    object propValue = pi.GetValue(item, null);

                    if (propValue == null)
                    {
                        output.Write("");
                    }
                    else
                    {
                        // collection property types
                        // create a pipe (|) separated string for collection properties
                        if (pi.PropertyType is ICollection)
                        {
                            var sb = new StringBuilder();

                            foreach (var enumItem in (propValue as IEnumerable))
                            {
                                sb.AppendFormat("{0}| ", enumItem.ToString());
                            }

                            output.Write(sb.ToString());
                        }
                        else
                        {
                            string value = propValue.ToString();

                            // if the data contains a separator char
                            // enclose it in double quotes
                            if (value.IndexOf(separator) != -1)
                            {
                                output.Write("\"" + value + "\"");
                            }
                            else
                            {
                                output.Write(value);
                            }
                        }
                    }
                }

                output.WriteLine();
            }
        }

        // the helper class used in the WriteCSV method
        class PropertyInfoWrapper
        {
            public int Order { get; set; }

            public PropertyInfo PropertyInfo { get; private set; }

            public PropertyInfoWrapper(PropertyInfo propertyInfo)
            {
                PropertyInfo = propertyInfo;
            }
        }
    }
}

With the above code in place, generating a csv from an IEnumerable is a one-liner.

var cars = new List<Car>();

cars.Add(new Car { Make = "Chevy", Year = 2004});
cars.Add(new Car { Make = "Ford", Year = 2005});

using(var writer = new StringWriter())
{
    Helper.WriteCSV(data, writer, headerRow);

    string csv = writer.ToString();
}

It gets better with an extension method.

namespace App.Web.Helpers  
{
    public static class Extensions
    {
        public static string ToCsv(
            this IEnumerable data,
            bool headerRow = false)
        {
            using(var writer = new StringWriter())
            {
                Helper.WriteCSV(data, writer, headerRow);

                return writer.ToString();
            }
        }
    }
}
// no header row
string csv = cars.ToCsv();

// with header row
string csvWithHeader = cars.ToCsv(true);

Related:

http://www.vickram.me/download-csv-using-asp-net-web-api