Creating Custom CSVMediaTypeFormatter In ASP.NET Web API for Comma-Separated Values (CSV) Format

In this post, we will see how to create a custom CSVMediaTypeFormatter in ASP.NET Web API for comma-separated values (CSV) format
2012-03-22 10:09
Tugberk Ugurlu


As I tried to explain on my previous MediaTypeFormatters With MediaTypeMappings post, formatters play a huge role inside the ASP.NET Web API processing pipeline. As Web API framework programming model is so similar to MVC framework, I kind of want to see formatters as views. Formatters handles serializing and deserializing strongly-typed objects into specific format.

I wanted to create CSVMediaTypeFormatter to hook it up for list of objects and I managed to get it working. After I created it, I saw the great Media Formatters post on ASP.NET web site which does the same thing. I was like "Man, come on!" and I noticed that formatter meant to be used with a specific object, so I figured there is still a validity in my implementation.

Here is the drill:

First of all we need to create a class which will be derived from MediaTypeFormatter abstract class. Here is the class with its constructors:

public class CSVMediaTypeFormatter : MediaTypeFormatter {

    public CSVMediaTypeFormatter() {

        SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/csv"));
    }
    
    public CSVMediaTypeFormatter(
        MediaTypeMapping mediaTypeMapping) : this() {

        MediaTypeMappings.Add(mediaTypeMapping);
    }
    
    public CSVMediaTypeFormatter(
        IEnumerable<MediaTypeMapping> mediaTypeMappings) : this() {

        foreach (var mediaTypeMapping in mediaTypeMappings) {
            MediaTypeMappings.Add(mediaTypeMapping);
        }
    }
}

Above, no matter which constructor you use, we always add text/csv media type to be supported for this formatter. We also allow custom MediaTypeMappings to be injected.

Now, we need to override two methods: MediaTypeFormatter.CanWriteType and MediaTypeFormatter.OnWriteToStreamAsync.

First of all, here is the CanWriteType method implementation. What this method needs to do is to determine if the type of the object is supported with this formatter or not in order to write it.

protected override bool CanWriteType(Type type) {

    if (type == null)
        throw new ArgumentNullException("type");

    return isTypeOfIEnumerable(type);
}

private bool isTypeOfIEnumerable(Type type) {

    foreach (Type interfaceType in type.GetInterfaces()) {

        if (interfaceType == typeof(IEnumerable))
            return true;
    }

    return false;
}

What this does here is to check if the object has implemented the IEnumerable interface. If so, then it is cool with that and can format the object. If not, it will return false and framework will ignore this formatter for that particular request.

And finally, here is the actual implementation. We need to do some work with reflection here in order to get the property names and values out of the value parameter which is a type of object:

protected override Task OnWriteToStreamAsync(
    Type type,
    object value,
    Stream stream,
    HttpContentHeaders contentHeaders,
    FormatterContext formatterContext,
    TransportContext transportContext) {

    writeStream(type, value, stream, contentHeaders);
    var tcs = new TaskCompletionSource<int>();
    tcs.SetResult(0);
    return tcs.Task;
}

private void writeStream(Type type, object value, Stream stream, HttpContentHeaders contentHeaders) {

    //NOTE: We have check the type inside CanWriteType method
    //If request comes this far, the type is IEnumerable. We are safe.

    Type itemType = type.GetGenericArguments()[0];

    StringWriter _stringWriter = new StringWriter();

    _stringWriter.WriteLine(
        string.Join<string>(
            ",", itemType.GetProperties().Select(x => x.Name )
        )
    );

    foreach (var obj in (IEnumerable<object>)value) {

        var vals = obj.GetType().GetProperties().Select(
            pi => new { 
                Value = pi.GetValue(obj, null)
            }
        );

        string _valueLine = string.Empty;

        foreach (var val in vals) {

            if (val.Value != null) {

                var _val = val.Value.ToString();

                //Check if the value contans a comma and place it in quotes if so
                if (_val.Contains(","))
                    _val = string.Concat("\"", _val, "\"");

                //Replace any \r or \n special characters from a new line with a space
                if (_val.Contains("\r"))
                    _val = _val.Replace("\r", " ");
                if (_val.Contains("\n"))
                    _val = _val.Replace("\n", " ");

                _valueLine = string.Concat(_valueLine, _val, ",");

            } else {

                _valueLine = string.Concat(string.Empty, ",");
            }
        }

        _stringWriter.WriteLine(_valueLine.TrimEnd(','));
    }

    var streamWriter = new StreamWriter(stream);
        streamWriter.Write(_stringWriter.ToString());
}

We are partially done. Now, we need to make use out of this. I registered this formatter into the pipeline with the following code inside Global.asax Application_Start method:

GlobalConfiguration.Configuration.Formatters.Add(
    new CSVMediaTypeFormatter(
        new  QueryStringMapping("format", "csv", "text/csv")
    )
);

On my sample application, when you navigate to /api/cars?format=csv, it will get you a CSV file but without an extension. Go ahead and add the csv extension. Then, open it with Excel and you should see something similar to below:

image

This implementation is also on my ASP.NET Web API package (TugberkUg.Web.Http) and you can get it via Nuget:

PM> Install-Package TugberkUg.Web.Http -Pre

This package contains other stuff related to ASP.NET Web API. You can check out the source code on https://github.com/tugberkugurlu/ASPNETWebAPISamples/tree/master/TugberkUg.Web.Http/src/TugberkUg.Web.Http.

The sample I used here is also on GitHub:https://github.com/tugberkugurlu/ASPNETWebAPISamples/tree/master/TugberkUg.Web.Http/src/samples/CSVMediaTypeFormatterSample

There are some caveats, though. If your class has nested custom types, then this one does not support that. You will see that, type of the class will be printed under the particular column.



Comments

Jim
by Jim on Wednesday, Mar 28 2012 09:33:15 +03:00

If you override OnGetResponseHeaders, you can set the Content-Disposition header and specify a filename and extension (maybe a static filename, like 'data.csv' or potentially dynamic based on the object that you are serializing to CSV).

I came up with a solution to this on the ASP.NET Web API forum: http://forums.asp.net/post/4890526.aspx

Tugberk
by Tugberk on Wednesday, Mar 28 2012 11:47:24 +03:00

@Jim

Thanks! I tried doing someting smilar but never got it working and stop chasing it down. Your solution is the reasonable one.

Cecil
by Cecil on Wednesday, Apr 18 2012 21:19:20 +03:00

How come only IEnumerable? Why not support single instances of objects also ?

SondreB
by SondreB on Thursday, Jun 21 2012 10:35:06 +03:00

Thanks for a great sample, but unfortunlately I'm was unable to get any data rendered on the output stream? While debugging, everything works and looks fine, yet the HTTP results are empty. The last line of the writeStream method executes without problems, the _streamWriter.ToString() returns data. But, nothing came out.

The solution was to run the .Flush method on the streamwriter.Flush() at the very end. This ensures the content of the stream is written out to the browser.

SondreB
by SondreB on Thursday, Jun 21 2012 10:51:17 +03:00

There is actually another bug in the code. When you check for val.Value, in the else, you do string.Concat with string.Empty instead of _valueLine. That means whenever you hit a NULL value object, you reset the whole line of values.

This is wrong:

_valueLine = string.Concat(string.Empty, ",");

Should be corrected to:

_valueLine = string.Concat(_valueLine, ",");

Tugberk
by Tugberk on Thursday, Jun 21 2012 11:54:56 +03:00

@SondreB

thanks for pointing those issues. I will get it fixed ASAP.

Rainer
by Rainer on Thursday, Sep 06 2012 07:18:47 +03:00

The CanWriteType() method can be simplified like this:

private static readonly Type expectedType = typeof(IEnumerable<object>);

public override bool CanWriteType(Type type)
{
    bool canBeFormatted = expectedType.IsAssignableFrom(type);
    return canBeFormatted;
}
John
by John on Wednesday, Feb 27 2013 18:12:49 +02:00

 

with this new release: http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/supporting-odata-query-options

this post seems not work anymore, there is limitation of AllowedQueryOptions

New Comment