Bug, or "how do I do this", in Web API OData:
Using the nightly builds to test out $expand support - note that I really really want to switch to Web API OData over WCF Data Services, because the extension points are soo much better, but I can't justify doing so until I can provide equivalent functionality.
I want to inject `ODataValidationSettings` (so I can manage them centrally instead of in per-controller attributes). I can't do that using EntitySetController, so I had to create my own base class. Here's one implementation:
``` C#
public abstract class DbSetController<TEntity, TKey, TDbContext> : ODataController
where TEntity : class
where TDbContext : DbContext
{
private readonly ODataValidationSettings _queryValidationSettings;
private readonly TDbContext _dbContext;
protected DbSetController(ODataValidationSettings queryValidationSettings, TDbContext dbContext)
{
_queryValidationSettings = queryValidationSettings;
_dbContext = dbContext;
}
protected virtual ODataValidationSettings QueryValidationSettings
{
get { return _queryValidationSettings; }
}
public virtual IQueryable<TEntity> Get(ODataQueryOptions<TEntity> queryOptions)
{
queryOptions.Validate(QueryValidationSettings);
IQueryable queryApplied = queryOptions.ApplyTo(_dbContext.Set<TEntity>());
return queryApplied.Cast<TEntity>();
}
```
This works fine for simple queries, like http://localhost/odata/Status?$top=2 .
However, it breaks as soon as I add an $expand or $select clause - I get:
``` xml
<m:internalexception>
<m:message>Unable to cast the type 'System.Web.Http.OData.Query.Expressions.SelectExpandWrapper`1' to type 'Scrum.Model.WorkItem'. LINQ to Entities only supports casting EDM primitive or enumeration types.</m:message>
<m:type>System.NotSupportedException</m:type>
<m:stacktrace> at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.ValidateAndAdjustCastTypes(TypeUsage toType, TypeUsage fromType, Type toClrType, Type fromClrType)
at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.GetCastTargetType(TypeUsage fromType, Type toClrType, Type fromClrType, Boolean preserveCastForDateTime)
at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.CreateCastExpression(DbExpression source, Type toClrType, Type fromClrType)
at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.MethodCallTranslator.CastMethodTranslator.Translate(ExpressionConverter parent, MethodCallExpression call)
at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.MethodCallTranslator.SequenceMethodTranslator.Translate(ExpressionConverter parent, MethodCallExpression call, SequenceMethod sequenceMethod)
at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.MethodCallTranslator.TypedTranslate(ExpressionConverter parent, MethodCallExpression linq)
at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.TypedTranslator`1.Translate(ExpressionConverter parent, Expression linq)
at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.TranslateExpression(Expression linq)
at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.Convert()
at System.Data.Entity.Core.Objects.ELinq.ELinqQueryState.GetExecutionPlan(Nullable`1 forMergeOption)
at System.Data.Entity.Core.Objects.ObjectQuery`1.<>c__DisplayClassb.<GetResults>b__a()
at System.Data.Entity.Core.Objects.ObjectContext.ExecuteInTransaction[T](Func`1 func, Boolean throwOnExistingTransaction, Boolean startLocalTransaction, Boolean releaseConnectionOnSuccess)
at System.Data.Entity.Core.Objects.ObjectQuery`1.<>c__DisplayClassb.<GetResults>b__9()
at System.Data.Entity.SqlServer.DefaultSqlExecutionStrategy.ProtectedExecute[TResult](Func`1 func)
at System.Data.Entity.Infrastructure.ExecutionStrategy.Execute[TResult](Func`1 func)
at System.Data.Entity.Core.Objects.ObjectQuery`1.GetResults(Nullable`1 forMergeOption)
at System.Data.Entity.Core.Objects.ObjectQuery`1.<System.Collections.Generic.IEnumerable<T>.GetEnumerator>b__0()
at System.Lazy`1.CreateValue()
at System.Lazy`1.LazyInitValue()
at System.Lazy`1.get_Value()
at System.Data.Entity.Internal.LazyEnumerator`1.MoveNext()
at System.Web.Http.OData.Formatter.Serialization.ODataFeedSerializer.WriteFeed(IEnumerable enumerable, ODataWriter writer, ODataSerializerContext writeContext)
at System.Web.Http.OData.Formatter.Serialization.ODataFeedSerializer.WriteObjectInline(Object graph, ODataWriter writer, ODataSerializerContext writeContext)
at System.Web.Http.OData.Formatter.Serialization.ODataFeedSerializer.WriteObject(Object graph, ODataMessageWriter messageWriter, ODataSerializerContext writeContext)
at System.Web.Http.OData.Formatter.ODataMediaTypeFormatter.WriteToStream(Type type, Object value, Stream writeStream, HttpContent content, HttpContentHeaders contentHeaders)
at System.Web.Http.OData.Formatter.ODataMediaTypeFormatter.WriteToStreamAsync(Type type, Object value, Stream writeStream, HttpContent content, TransportContext transportContext)
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
at System.Web.Http.WebHost.HttpControllerHandler.<WriteBufferedResponseContentAsync>d__10.MoveNext()</m:stacktrace>
</m:internalexception>
```
Figuring that it may be an issue with entity framework not liking the other types in its expression tree, I tried this implementation:
``` C#
public virtual HttpResponseMessage Get(ODataQueryOptions<TEntity> queryOptions)
{
queryOptions.Validate(QueryValidationSettings);
IQueryable queryApplied = queryOptions.ApplyTo(_dbContext.Set<TEntity>());
var list = new List<object>(100);
foreach (var o in queryApplied)
{
list.Add(o);
}
return Request.CreateResponse(list.Cast<TEntity>());
}
```
This breaks as follows:
``` xml
<?xml version="1.0" encoding="utf-8"?>
<m:error xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
<m:code />
<m:message xml:lang="en-US">An error has occurred.</m:message>
<m:innererror>
<m:message>The 'ObjectContent`1' type failed to serialize the response body for content type 'application/json; charset=utf-8'.</m:message>
<m:type>System.InvalidOperationException</m:type>
<m:stacktrace></m:stacktrace>
<m:internalexception>
<m:message>Unable to cast object of type 'System.Web.Http.OData.Query.Expressions.SelectExpandWrapper`1[Scrum.Model.WorkItem]' to type 'Scrum.Model.WorkItem'.</m:message>
<m:type>System.InvalidCastException</m:type>
<m:stacktrace> at System.Linq.Enumerable.<CastIterator>d__b1`1.MoveNext()
at System.Web.Http.OData.Formatter.Serialization.ODataFeedSerializer.WriteFeed(IEnumerable enumerable, ODataWriter writer, ODataSerializerContext writeContext)
at System.Web.Http.OData.Formatter.Serialization.ODataFeedSerializer.WriteObjectInline(Object graph, ODataWriter writer, ODataSerializerContext writeContext)
at System.Web.Http.OData.Formatter.Serialization.ODataFeedSerializer.WriteObject(Object graph, ODataMessageWriter messageWriter, ODataSerializerContext writeContext)
at System.Web.Http.OData.Formatter.ODataMediaTypeFormatter.WriteToStream(Type type, Object value, Stream writeStream, HttpContent content, HttpContentHeaders contentHeaders)
at System.Web.Http.OData.Formatter.ODataMediaTypeFormatter.WriteToStreamAsync(Type type, Object value, Stream writeStream, HttpContent content, TransportContext transportContext)
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
at System.Web.Http.WebHost.HttpControllerHandler.<WriteBufferedResponseContentAsync>d__10.MoveNext()</m:stacktrace>
</m:internalexception>
</m:innererror>
</m:error>
```
It looks like I can extract the element values from `SelectExpandWrapper`, but it's not clear that's the right way to go - please point me at a better example if there's a better approach.
Comments: That is the expected behavior. Once $select or $expand is applied to IQueryable<TEntity>, the result is no longer IQueryable<TEntity>. It becomes IQueryable<SelectExpandWrapper<TEntity>> as $select and $expand are shape changing queries. I have some sample code for supporting $select and $expand using ODataQueryOptions<T> [here](https://aspnetwebstack.codeplex.com/wikipage?title=%24select%20and%20%24expand%20support&referringTitle=Specs). Please refer to sample number 4. It is slightly more complicated than usual due to the missing Request.CreateResponse that takes runtime type.
Using the nightly builds to test out $expand support - note that I really really want to switch to Web API OData over WCF Data Services, because the extension points are soo much better, but I can't justify doing so until I can provide equivalent functionality.
I want to inject `ODataValidationSettings` (so I can manage them centrally instead of in per-controller attributes). I can't do that using EntitySetController, so I had to create my own base class. Here's one implementation:
``` C#
public abstract class DbSetController<TEntity, TKey, TDbContext> : ODataController
where TEntity : class
where TDbContext : DbContext
{
private readonly ODataValidationSettings _queryValidationSettings;
private readonly TDbContext _dbContext;
protected DbSetController(ODataValidationSettings queryValidationSettings, TDbContext dbContext)
{
_queryValidationSettings = queryValidationSettings;
_dbContext = dbContext;
}
protected virtual ODataValidationSettings QueryValidationSettings
{
get { return _queryValidationSettings; }
}
public virtual IQueryable<TEntity> Get(ODataQueryOptions<TEntity> queryOptions)
{
queryOptions.Validate(QueryValidationSettings);
IQueryable queryApplied = queryOptions.ApplyTo(_dbContext.Set<TEntity>());
return queryApplied.Cast<TEntity>();
}
```
This works fine for simple queries, like http://localhost/odata/Status?$top=2 .
However, it breaks as soon as I add an $expand or $select clause - I get:
``` xml
<m:internalexception>
<m:message>Unable to cast the type 'System.Web.Http.OData.Query.Expressions.SelectExpandWrapper`1' to type 'Scrum.Model.WorkItem'. LINQ to Entities only supports casting EDM primitive or enumeration types.</m:message>
<m:type>System.NotSupportedException</m:type>
<m:stacktrace> at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.ValidateAndAdjustCastTypes(TypeUsage toType, TypeUsage fromType, Type toClrType, Type fromClrType)
at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.GetCastTargetType(TypeUsage fromType, Type toClrType, Type fromClrType, Boolean preserveCastForDateTime)
at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.CreateCastExpression(DbExpression source, Type toClrType, Type fromClrType)
at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.MethodCallTranslator.CastMethodTranslator.Translate(ExpressionConverter parent, MethodCallExpression call)
at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.MethodCallTranslator.SequenceMethodTranslator.Translate(ExpressionConverter parent, MethodCallExpression call, SequenceMethod sequenceMethod)
at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.MethodCallTranslator.TypedTranslate(ExpressionConverter parent, MethodCallExpression linq)
at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.TypedTranslator`1.Translate(ExpressionConverter parent, Expression linq)
at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.TranslateExpression(Expression linq)
at System.Data.Entity.Core.Objects.ELinq.ExpressionConverter.Convert()
at System.Data.Entity.Core.Objects.ELinq.ELinqQueryState.GetExecutionPlan(Nullable`1 forMergeOption)
at System.Data.Entity.Core.Objects.ObjectQuery`1.<>c__DisplayClassb.<GetResults>b__a()
at System.Data.Entity.Core.Objects.ObjectContext.ExecuteInTransaction[T](Func`1 func, Boolean throwOnExistingTransaction, Boolean startLocalTransaction, Boolean releaseConnectionOnSuccess)
at System.Data.Entity.Core.Objects.ObjectQuery`1.<>c__DisplayClassb.<GetResults>b__9()
at System.Data.Entity.SqlServer.DefaultSqlExecutionStrategy.ProtectedExecute[TResult](Func`1 func)
at System.Data.Entity.Infrastructure.ExecutionStrategy.Execute[TResult](Func`1 func)
at System.Data.Entity.Core.Objects.ObjectQuery`1.GetResults(Nullable`1 forMergeOption)
at System.Data.Entity.Core.Objects.ObjectQuery`1.<System.Collections.Generic.IEnumerable<T>.GetEnumerator>b__0()
at System.Lazy`1.CreateValue()
at System.Lazy`1.LazyInitValue()
at System.Lazy`1.get_Value()
at System.Data.Entity.Internal.LazyEnumerator`1.MoveNext()
at System.Web.Http.OData.Formatter.Serialization.ODataFeedSerializer.WriteFeed(IEnumerable enumerable, ODataWriter writer, ODataSerializerContext writeContext)
at System.Web.Http.OData.Formatter.Serialization.ODataFeedSerializer.WriteObjectInline(Object graph, ODataWriter writer, ODataSerializerContext writeContext)
at System.Web.Http.OData.Formatter.Serialization.ODataFeedSerializer.WriteObject(Object graph, ODataMessageWriter messageWriter, ODataSerializerContext writeContext)
at System.Web.Http.OData.Formatter.ODataMediaTypeFormatter.WriteToStream(Type type, Object value, Stream writeStream, HttpContent content, HttpContentHeaders contentHeaders)
at System.Web.Http.OData.Formatter.ODataMediaTypeFormatter.WriteToStreamAsync(Type type, Object value, Stream writeStream, HttpContent content, TransportContext transportContext)
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
at System.Web.Http.WebHost.HttpControllerHandler.<WriteBufferedResponseContentAsync>d__10.MoveNext()</m:stacktrace>
</m:internalexception>
```
Figuring that it may be an issue with entity framework not liking the other types in its expression tree, I tried this implementation:
``` C#
public virtual HttpResponseMessage Get(ODataQueryOptions<TEntity> queryOptions)
{
queryOptions.Validate(QueryValidationSettings);
IQueryable queryApplied = queryOptions.ApplyTo(_dbContext.Set<TEntity>());
var list = new List<object>(100);
foreach (var o in queryApplied)
{
list.Add(o);
}
return Request.CreateResponse(list.Cast<TEntity>());
}
```
This breaks as follows:
``` xml
<?xml version="1.0" encoding="utf-8"?>
<m:error xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
<m:code />
<m:message xml:lang="en-US">An error has occurred.</m:message>
<m:innererror>
<m:message>The 'ObjectContent`1' type failed to serialize the response body for content type 'application/json; charset=utf-8'.</m:message>
<m:type>System.InvalidOperationException</m:type>
<m:stacktrace></m:stacktrace>
<m:internalexception>
<m:message>Unable to cast object of type 'System.Web.Http.OData.Query.Expressions.SelectExpandWrapper`1[Scrum.Model.WorkItem]' to type 'Scrum.Model.WorkItem'.</m:message>
<m:type>System.InvalidCastException</m:type>
<m:stacktrace> at System.Linq.Enumerable.<CastIterator>d__b1`1.MoveNext()
at System.Web.Http.OData.Formatter.Serialization.ODataFeedSerializer.WriteFeed(IEnumerable enumerable, ODataWriter writer, ODataSerializerContext writeContext)
at System.Web.Http.OData.Formatter.Serialization.ODataFeedSerializer.WriteObjectInline(Object graph, ODataWriter writer, ODataSerializerContext writeContext)
at System.Web.Http.OData.Formatter.Serialization.ODataFeedSerializer.WriteObject(Object graph, ODataMessageWriter messageWriter, ODataSerializerContext writeContext)
at System.Web.Http.OData.Formatter.ODataMediaTypeFormatter.WriteToStream(Type type, Object value, Stream writeStream, HttpContent content, HttpContentHeaders contentHeaders)
at System.Web.Http.OData.Formatter.ODataMediaTypeFormatter.WriteToStreamAsync(Type type, Object value, Stream writeStream, HttpContent content, TransportContext transportContext)
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
at System.Web.Http.WebHost.HttpControllerHandler.<WriteBufferedResponseContentAsync>d__10.MoveNext()</m:stacktrace>
</m:internalexception>
</m:innererror>
</m:error>
```
It looks like I can extract the element values from `SelectExpandWrapper`, but it's not clear that's the right way to go - please point me at a better example if there's a better approach.
Comments: That is the expected behavior. Once $select or $expand is applied to IQueryable<TEntity>, the result is no longer IQueryable<TEntity>. It becomes IQueryable<SelectExpandWrapper<TEntity>> as $select and $expand are shape changing queries. I have some sample code for supporting $select and $expand using ODataQueryOptions<T> [here](https://aspnetwebstack.codeplex.com/wikipage?title=%24select%20and%20%24expand%20support&referringTitle=Specs). Please refer to sample number 4. It is slightly more complicated than usual due to the missing Request.CreateResponse that takes runtime type.