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.
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.
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.
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.
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... }
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:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With