This series of code chunks provides a full picture of the CSV Generation stack. The CSV Generation stack is used
in several places throughout our application, from the Revenue Reports, to families viewing their students
and invoices.
Since we interpreted the requirements as having to build a hands-off system, we chose to send the monthly
reports as CSVs in an email to the administrator. Because of this, I chose to generate CSV reports on the
backend to prevent duplicate code between report viewing and sending. This operates with the help of CsvHelper,
a NuGet package that helps generate CSVs from models.
public static byte[] GenerateCsv<T>(IList<T> itemsToWriteList, ClassMap map = null)
{
using (var memoryStream = new MemoryStream())
{
TextWriter writer = new StreamWriter(memoryStream);
var csv = new CsvWriter(writer);
var records = itemsToWriteList;
if (map != null)
{
csv.Configuration.RegisterClassMap(map);
}
csv.WriteRecords(records);
writer.Flush();
return memoryStream.ToArray();
}
}
GenerateCsv
is where the CSV actually gets generated in the form of a byte array to prepare for sending it as an HTTP response. The goal of this method was to make it as abstract as possible since several parts of the application use this feature. This method belongs in the utility project to allow for reuse in multiple components. it prefers interfaces over their implementations by using an IList
, so that if we choose to use something like a LinkedList
in the future, we are able to without much refactoring. It also allows for an IList of any type <T>
to be generated into a CSV. Finally, it makes use of C#'s default parameters so that even developers not using a model with a ClassMap can use this method.
Within the function, this code utilizes using
statements to control resource usage. Additionally, writer.Flush()
is called to prevent the CsvWriter
from becoming overloaded.
public class TotalProfitReportItem
{
public string StudentFirstName { get; set; }
public string StudentLastName { get; set; }
public string FeeType { get; set; }
public decimal Amount { get; set; }
}
public sealed class TotalProfitClassMap : ClassMap<TotalProfitReportItem>
{
public TotalProfitClassMap()
{
Map(m => m.StudentFirstName).Name("First Name");
Map(m => m.StudentLastName).Name("Last Name");
Map(m => m.FeeType).Name("Fee Description");
Map(m => m.Amount).Name("Amount");
}
}
TotalProfitReportItem
is an example of a model class that describes the columns of one of our revenue reports.
TotalProfitReportItem
demonstrates low coupling by separating itself from any of the other reports. By creating a model for each
report type, it becomes easy to change what information is displayed in the report and create new reports.
ClassMaps are a feature in the CsvHelper library that allow you to customize how information is displayed
in the CSV. For this, I used the ClassMap to rename the columsn. Additionally, this class demonstrates high
cohesion by defining the CSV ClassMaps in the same place as the model definition.
public class ResponseFactory
{
public static HttpResponseMessage ConstructCsvResponse(byte[] csvContentBytes)
{
var result = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new ByteArrayContent(csvContentBytes)
};
result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
result.Content.Headers.ContentLength = csvContentBytes.Length;
return result;
}
}
The final chunk in this code sample is the ConstructCsvResponse
method. This method custom creates the header and response of the CSV byte array using UTF-8 and specifying the length of the response. It displays good software engineering principles by making the representation of the data consistent both in the back-end and the front-end.