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 => r.CurrentReadQuality != ReadQuality.Estimate)
.OrderBy(r => r.CurrentReadDate)
.ThenBy(r => r.DeviceKey)
.ThenBy(r => 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 => 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");
}
1 Answer 1
I modified the formatting part for better readability and maintainability, at the cost of lower performance:
var columns = new Dictionary<string, Func<Reads, object>>
{
["Read Date"] = x => x.CurrentReadDate,
["Device"] = x => x.DeviceKey,
["Suffix"] = x => x.Suffix,
["Read"] = x => 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) => $"{{{i}}}").Skip(2));
var csv = new StringBuilder()
.AppendFormat(format, columns.Keys.ToArray())
.AppendLine();
foreach (var read in reads)
{
var values = columns.Values.Select(selector => 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 => x.CurrentReadDate.ToString("yyyyMMddThhmm")
string.Format()
? \$\endgroup\$.ToCsv()
: docs.servicestack.net/csv-format \$\endgroup\$string.Format
is awfully hard to follow given the large number of parameters. I would suggest something like$"{x.CurrentReadDate},{x.DeviceKey}" ...
. This would also help avoid errors due to matching up parameter indexes if this code was later modified. \$\endgroup\$