27

I'm trying to create a MediaTypeFormatter to handle text/csv but running into a few problems when using $expand in the OData query.

Query:

http://localhost/RestBlog/api/Blogs/121?$expand=Comments

Controller:

[EnableQuery]
public IQueryable<Blog> GetBlog(int id)
{
    return DbCtx.Blog.Where(x => x.blogID == id);
}

In my media type formatter:

private static MethodInfo _createStreamWriter =
        typeof(CsvFormatter)
        .GetMethods(BindingFlags.Static | BindingFlags.NonPublic)
        .Single(m => m.Name == "StreamWriter");

internal static void StreamWriter<T, X>(T results)
{
    var queryableResult = results as IQueryable<X>;
    if (queryableResult != null)
    {
        var actualResults = queryableResult.ToList<X>();
    }
}

public override void WriteToStream(Type type, object value,
    Stream writeStream, HttpContent content)
{
    Type genericType = type.GetGenericArguments()[0];
    _createStreamWriter.MakeGenericMethod(
               new Type[] { value.GetType(), genericType })
                .Invoke(null, new object[] { value }
       );
}

Note that the type of value is System.Data.Entity.Infrastructure.DbQuery<System.Web.Http.OData.Query.Expressions.SelectExpandBinder.SelectAllAndExpand<Rest.Blog>> which means that it doesn't work.

The type of value should be IQueryable but upon casting it returns null.

When making a query without the $expand things work a lot more sensibly. What am I doing wrong?

I'm just trying to get at the data before even outputting as CSV, so guidance would be greatly appreciated.

Yuri
  • 2,820
  • 4
  • 28
  • 40
geekboyUK
  • 498
  • 4
  • 14

2 Answers2

6

If you look at the source code for OData Web API, you will see that SelectExpandBinder.SelectAllAndExpand is a subclass of the generic class SelectExpandWrapper(TEntity) :

private class SelectAllAndExpand<TEntity> : SelectExpandWrapper<TEntity>
{
}

which itself is a subclass of non-generic SelectExpandWrapper:

internal class SelectExpandWrapper<TElement> : SelectExpandWrapper
{
    // Implementation...
}

which in turn implements IEdmEntityObject and ISelectExpandWrapper:

internal abstract class SelectExpandWrapper : IEdmEntityObject, ISelectExpandWrapper
{
    // Implementation...
}

This means that you have access to the ISelectExpandWrapper.ToDictionary method and can use it to get at the properties of the underlying entity:

public interface ISelectExpandWrapper
{
    IDictionary<string, object> ToDictionary();
    IDictionary<string, object> ToDictionary(Func<IEdmModel, IEdmStructuredType, IPropertyMapper> propertyMapperProvider);
}

Indeed this is how serialization to JSON is implemented in the framework as can be seen from SelectExpandWrapperConverter:

internal class SelectExpandWrapperConverter : JsonConverter
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        ISelectExpandWrapper selectExpandWrapper = value as ISelectExpandWrapper;
        if (selectExpandWrapper != null)
        {
            serializer.Serialize(writer, selectExpandWrapper.ToDictionary(_mapperProvider));
        }
    }

    // Other methods...
}
Søren Boisen
  • 1,669
  • 22
  • 41
  • 1
    Would be useful to add the applicable sections from the referenced pages as URLs do not live forever (as is the case with these two). – Will Mar 22 '21 at 13:20
  • @Will Trouble is that this is all undocumented and must be gleaned from looking at the source code. I added excerpts from the source code and fixed the links. Hopefully the answer will better stand the test of time now. – Søren Boisen Mar 23 '21 at 23:15
2

I was googled when i face that issue in my task.. i have clean implementation from this thread

first you need verify edm modelbuilder in proper way for expand objects

you have to register edm model for blog and foreign key releations.then only it will sucess

Example

  ODataModelBuilder builder = new ODataConventionModelBuilder();
        builder.EntitySet<Blog>("blog");
        builder.EntitySet<Profile>("profile");//ForeignKey releations of blog
        builder.EntitySet<user>("user");//ForeignKey releations of profile
        config.MapODataServiceRoute(
            routeName: "ODataRoute",
            routePrefix: null,
            model: builder.GetEdmModel());

Then you need develop this formatter ..example source code us here

applogies for my english..

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:

Jagadeesh Govindaraj
  • 6,977
  • 6
  • 32
  • 52
  • This is closer, but still not quite what I need. If the object being converted has a foreign key relation to another entity, the call val.Value.ToString() dumps it all in one string as "Property1=Value1 Property2=Value2" which isn't really useful in a csv. – Geoff May 27 '15 at 13:13
  • And if you specify $expand in the query string, its even worse since then obj.GetType() returns System.Web.OData.Query.Expressions.SelectExpandBinder.SelectAllAndExpand<> so it doesn't get the right properties for the actual entity object. – Geoff May 27 '15 at 13:18