Convert IEnumerable to CSV I have an IEnumerable of a view model that I need to convert to CSV and send back as a file response. I have written it out quite manually and I'm just seeking code review and suggestions on a more elegant solution, or alternatively if this looks acceptable, confirmation of that:

    public ActionResult ExportBasicReads(string nmi, DateTime fromDate, DateTime toDate)
    {
        var reads = _dataService.GetBasicReads(nmi, fromDate, toDate)
                    .Where(r =&gt; r.CurrentReadQuality != ReadQuality.Estimate)
                    .OrderBy(r =&gt; r.CurrentReadDate)
                    .ThenBy(r =&gt; r.DeviceKey)
                    .ThenBy(r =&gt; r.ValidationCode);

        StringBuilder csv = new StringBuilder();

        csv.AppendLine("Read Date,Device : Suffix,Read,Quality,Tran Code,Service Order,Previous Read Date,Previous Read,Previous Quality," +
            "Previous Tran Code,Dial Diff,Quantity,UOM,Dial Factor,Direction,Validation,From Date,To Date,Kwh,Avg Daily Load,Profile Name," +
            "Profle Area,Reading Timestamp,Update Date,ChangedBy,ChangeType,ChangeComment");

        reads.ForEach(x =&gt; csv.AppendLine(
                string.Format("{0},{1} : {2},{3},{4},{5},{6},{7},{8},{9},{10},{11},{12},{13},{14},{15},{16},{17},{18},{19},{20},{21},{22}," +
                "{23},{24},{25},{26},{27}",
                    x.CurrentReadDate,
                    x.DeviceKey,
                    x.Suffix,
                    x.CurrentReadRawValue,
                    x.CurrentReadQualityDesc,
                    x.CurrentReadTransactionCode,
                    x.CurrentServiceOrder,
                    x.PreviousReadDate,
                    x.PreviousReadRawValue,
                    x.PreviousReadQualityDesc,
                    x.PreviousReadTransactionCode,
                    x.DialDifference,
                    x.Quantity,
                    x.UOMDesc,
                    x.Multiplier,
                    x.DirectionDesc,
                    x.ValidationDesc,
                    x.EffectiveFromDateTime,
                    x.EffectiveToDateTime,
                    x.Kwh,
                    x.AverageDailyLoad,
                    x.ProfileName,
                    x.ProfileArea,
                    x.EffectiveFromDateTime,
                    x.UpdateTimestamp,
                    x.ChangedBy,
                    x.ChangeType,
                    x.ChangeComment
                )
            ));            

        return File(Encoding.ASCII.GetBytes(csv.ToString()), "text/csv", "BasicReadsExport.csv");
    }
I modified the formatting part for better readability and maintainability, at the cost of lower performance:

var columns = new Dictionary&lt;string, Func&lt;Reads, object&gt;&gt;
{
    ["Read Date"] = x =&gt; x.CurrentReadDate,
    ["Device"] = x =&gt; x.DeviceKey,

    ["Suffix"] = x =&gt; x.Suffix,
    ["Read"] = x =&gt; x.CurrentReadRawValue,
    // add the rest of headers/property selectors below
};
//var format = "{0},{1} : {2},{3},{4},{5},{6},{7},{8},{9},{10},{11},{12},{13},{14},{15},{16},{17},{18},{19},{20},{21},{22},{23},{24},{25},{26},{27}";
var format = "{0}{1} : " + string.Join(",", columns.Select((_, i) =&gt; $"{{{i}}}").Skip(2));

var csv = new StringBuilder()
    .AppendFormat(format, columns.Keys.ToArray())
    .AppendLine();
foreach (var read in reads)
{
    var values = columns.Values.Select(selector =&gt; selector(read));
    csv
        .AppendFormat(format, values.ToArray())
        .AppendLine();
}


The advantage of this approach is that you can add/move/remove the columns without having to worry about updating the header and the format, and if they are kept in sync or not.

Note:


You can also revert to the constant format if it becomes more complex.
The property selector can be chained if you need special format on individual property, like: x =&gt; x.CurrentReadDate.ToString("yyyyMMddThhmm")