First time here? You are looking at the most recent posts. You may also want to check out older archives or the tag cloud. Please leave a comment, ask a question and consider subscribing to the latest posts via RSS. Thank you for visiting! (hide this)

Continuing with my series of posts inspired by the work done on the Web.NET Conference web site, after telling you why you should not use Boolean fields when modeling your objects, today I want to share with you an ActionResult I wrote to get a CSV from a generic list of object.

I needed to download the list of all attendees in csv format so that I could import them into Excel for doing various free-form analysis: I looked around the net to see if something was available but I found nothing, so I decided to write it myself and do it the most reusable way possible.

I’m not just going to give you the code, but I’d also try and comment the most important part of the class.

Extend ActionResult

Obviously, being used as result returned from an action, the class has to extend ActionResult, and in this case, since what I’m returning is a file, I’m extending FileResult: this provides all the standard file-related tasks, like setting the content type and setting the filename that will be suggested when saving it.

Constructor and declaration

The values for these two properties will be set in the ActionResult’s constructor:

public class CsvActionResult<T> : FileResult
{
    private readonly IList<T> _list;
    private readonly char _separator;

    public CsvActionResult(IList<T> list,
        string fileDownloadName, 
        char separator=',')
        : base("text/csv")
    {
        _list = list;
        FileDownloadName = fileDownloadName;
        _separator = separator;
    }
...

I’m also passing in an optional separator because in many countries the the separator for fields is not a comma, but a semi-column.

Override WriteFile

In order to extend FileResult, you also need to override the WriteFile method, which is the one responsible of getting the value of the file and sending it to the user. The standard implementation just takes a file from the disk and send it to the browser: in this case I have to write to the OutputStream directly the value of my list of objects.

protected override void WriteFile(HttpResponseBase response)
{
    var outputStream = response.OutputStream;
    using (var memoryStream = new MemoryStream())
    {
        WriteList(memoryStream);
        outputStream.Write(memoryStream.GetBuffer(), 0, (int)memoryStream.Length);
    }
}

Discovering the property names at runtime

The csv file needs to have the list of all the columns’ names so that Excel can give a name to it once imported: since the action results accepts any object I needed to find out the name of the property. This was easily done via reflection and passed to a function that writes it in the OutputStream of the response.

foreach (MemberInfo member in typeof(T).GetProperties())
{
    WriteValue(streamWriter, member.Name);
}

The write value method is an utility method that formats the value in the proper way and with the correct separator. I’ll show this later in the post.

Writing each object of the list

After having written the header, always via reflection I get the values of the properties, and write them in the OutputStream.

foreach (T line in _list)
{
    foreach (MemberInfo member in typeof(T).GetProperties())
    {
        WriteValue(streamWriter, GetPropertyValue(line, member.Name));
    }
    streamWriter.WriteLine();
}

If a property is of a complex type, and you still want to export it into the csv file, all you need to do is override the ToString method to format it into a string. Here is the GetPropertyValue method that does the actual work of retrieving the value from object.

public static string GetPropertyValue(object src, string propName)
{
    return src.GetType().GetProperty(propName).GetValue(src, null).ToString()??"";
}

Formatting the property value

Care must be given when writing the actual string otherwise Excel (or any other csv parser) will not be able to get the data correctly: the value must be enclosed in double quotes, and all double quotes inside the value must be escaped. And furthermore the separator has to be different based on the culture of the user, but to make it simple I take the value supplied during the instantiation of the action result.

private void WriteValue(StreamWriter writer, String value)
{
    writer.Write("\"");
    writer.Write(value.Replace("\"", "\"\""));
    writer.Write("\"" + _separator);
}

How to use it

Using the action result is pretty easy: you just create a new instance of the class, and pass it a IList of something. And then return the class in you action method.

public ActionResult CsvList()
{
    var model = CreateUserListViewModel();
    return new CsvActionResult<UserListViewModelItem>(model.Items, "UserExport.csv");
}

Get the complete code

You can get the complete code of the class on this gist. I’ll be creating a NuGet package with binaries and some enhancements soon.

posted on Thursday, November 22, 2012 11:48 AM

Comments on this entry:

# re: How to return a CSV file with ASP.NET MVC

Left by marco at 11/23/2012 12:15 PM

nice article, helpful insight on how to write a custom FileResult.
To generate a csv and stream it I use FileHelpers, easy and convenient.

# re: How to return a CSV file with ASP.NET MVC

Left by Michael at 11/23/2012 1:36 PM

return src.GetType().GetProperty(propName).GetValue(src, null).ToString()??"";

The ?? operator should be used before ToString()

# re: How to return a CSV file with ASP.NET MVC

Left by Andy at 11/23/2012 1:44 PM

I agree with Marco. I used to use a custom reflection based approach until I moved to the more flexible FileHelpers.


public class CsvResult<T> : ActionResult
{
private string slug;
private IEnumerable<T> results;

public override void ExecuteResult(ControllerContext context)
{
var fileHelperEngine = new FileHelperEngine<T>();
var response = context.HttpContext.Response;

response.ContentType = "text/csv";
response.AppendHeader("Content-Disposition", string.Format("attachment;filename={0}-{1:yyyy-MM-dd-HH-mm-ss}.csv", slug, DateTime.UtcNow));

using (var textWriter = new StreamWriter(response.OutputStream))
{
fileHelperEngine.WriteStream(textWriter, results);
}
}

public CsvResult(string slug, IEnumerable<T> results)
{
this.slug = slug;
this.results = results;
}
}

Comments have been closed on this topic.