In this post, we will see how to create a custom CSVMediaTypeFormatter in ASP.NET Web API for comma-separated values (CSV) format
@ 03-22-2012
by 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.




Hi 🙋🏻‍♂️ I'm Tugberk Ugurlu.
Coder 👨🏻‍💻, Speaker 🗣, Author 📚, Microsoft MVP 🕸, Blogger 💻, Software Engineering at Deliveroo 🍕🍜🌯, F1 fan 🏎🚀, Loves travelling 🛫🛬
Lives in Cambridge, UK 🏡