Saturday 19 December 2015

C# CSV Library and HTTP Handlers

A colleague and I was recently looking at some old ASP.NET WebForms code which generated a potentially large CSV string and returned it in a response to a HTTP request. The original code was placed in a standard ASP.NET WebForms page in the OnLoad event handler. As the code was working with the HttpResponse object and writing the CSV directly to the HTTP output stream, we determined that this code was better placed in a HTTP Handler. The primary advantage of using a HTTP handler to generate the file is that we can bypass the unnecessary ASP.NET WebForms page life cycle.

The existing code was using a StringBuilder to generate the CSV string in the page OnLoad event handler. As you may imagine, this looked messy and was a pain to maintain. To abstract the CSV string creation logic, my colleague introduced me to a useful CSV library called LINQ to CSV. The project page link for this library is here and the library is available as a NuGet package.

The LINQ to CSV library enabled us to decorate some properties in a model class with attributes that specify how to output the property in CSV form. The example below (from the library project page) shows how to apply the attributes:

public class Product
{
    [CsvColumn(Name = "ProductName", FieldIndex = 1)]
    public string Name { get; set; }
    [CsvColumn(FieldIndex = 2, OutputFormat = "dd MMM HH:mm:ss")]
    public DateTime LaunchDate { get; set; }
    [CsvColumn(FieldIndex = 3, CanBeNull = false, OutputFormat = "C")]
    public decimal Price { get; set; }
    [CsvColumn(FieldIndex = 4)]
    public string Country { get; set; }
    [CsvColumn(FieldIndex = 5)]
    public string Description { get; set; }
}

Once your model class is decorated, you can then use other classes of the library to generate a CSV representation of a collection containing your model instances. This representation can be output to disk as demonstrated in the code example below:

IList<Product> products = new List<Product>();
products.Add(
    new Product
    {
        Name = "Samung Galaxy S6",
        LaunchDate = DateTime.Today,
        Price = 500,
        Country = "United Kingdom",
        Description = "This is a Samsung phone product"
    }
);

products.Add(
    new Product
    {
        Name = "Apple iPhone 5",
        LaunchDate = DateTime.Today,
        Price = 600,
        Country = "United States",
        Description = "This is an Apple phone product"
    }
);

CsvFileDescription inputFileDescription = new CsvFileDescription();
inputFileDescription.SeparatorChar = ',';
inputFileDescription.FirstLineHasColumnNames = true;

CsvContext csvContext = new CsvContext();
csvContext.Write(products, @"C:\Products.csv");

Usefully, the CsvContext class of the library also supports an overload to the Write method which accepts a TextWriter stream. Passing a TextWriter stream to this overload results in the library outputing the CSV representation to your stream rather than a file on disk. In our case, we used a StreamWriter that wrote to a MemoryStream. Once the CSV file content was in the MemoryStream instance, we sent the stream contents in the HTTP response from the HTTP handler. The full HTTP handler code is below (using the example product data).

public class ProductsExportHandler : IHttpHandler
{
    public bool IsReusable { get { return true; } }

    public void ProcessRequest(HttpContext context)
    {
        context.Response.AddHeader(
            "content-disposition"
            "attachment;filename=BulkExport.csv"
        );
        context.Response.Cache.SetCacheability(HttpCacheability.NoCache);
        context.Response.ContentType = "application/vnd.text";

        IList<Product> products = new List<Product>();
        // Add products...
            
        var inputFileDescription = new CsvFileDescription();
        inputFileDescription.SeparatorChar = ',';
        inputFileDescription.FirstLineHasColumnNames = true;
            
        using (var memoryStream = new MemoryStream())
        {
            using (var streamWriter = new StreamWriter(memoryStream))
            {
                var csvContext = new CsvContext();
                csvContext.Write(products, streamWriter);
                    
                streamWriter.Flush();
                    
                context.Response.BinaryWrite(memoryStream.GetBuffer());
            }
        }
    }
}