Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Web API OData media type formatter when using $expand

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.

like image 705
geekboyUK Avatar asked Sep 22 '14 14:09

geekboyUK


People also ask

How do you use $expand in OData?

OData query option $expand is used to read multiple entities or entity sets in a single service call instead of two different calls. Prerequisite, entity sets which are used should be associated. To know about Association in OData service click here.

What is $value in OData?

The $value option is used to get individual properties of an Entity. There are two ways to get individual properties from an entity. We can get the response in either OData format or get the raw value of the property. We need to add method to the controller named GetProperty here property is a name of the property.

What is $select in OData?

The $select option specifies a subset of properties to include in the response body. For example, to get only the name and price of each product, use the following query: Console Copy. GET http://localhost/odata/Products?$select=Price,Name.


2 Answers

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... } 
like image 125
Søren Boisen Avatar answered Sep 30 '22 21:09

Søren Boisen


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:

like image 34
Jagadeesh Govindaraj Avatar answered Sep 30 '22 21:09

Jagadeesh Govindaraj