WebAPI, OData, EF5 -- any/all issue

Topics: ASP.NET Web API
Apr 16, 2013 at 7:16 PM
Edited Apr 16, 2013 at 7:25 PM
I have a fairly simple collection of items I'm trying to filter based in child tag object content. Other, simpler, filtering works fine but I can't get 'any' or 'all' to work for the life of me. Is it a bug or am I doing something wrong? I'm using the latest nightly of Web API and OData (130416 as of writing).

Also, side question: is there any good blog, text file (... or anything!) to keep up with changes made to Odata/WebAPI on a nightly/weekly/monthly basis. I find OData to be ever changing yet very poorly documented.

Thanks!


I used EF5, model-first (so my code was generated for me) but the general class structure is like so:
public class Item { 
   ...,
   public EntityCollection<Tag> Tags { ... } // generated by EF
}

public class Tag {
   ...,
   public string Name { ... } // generated by EF
}
Then the WebAPI method (ignore .Map<>(), it's my own method for mapping to a DTO after filtering):
// GET api/items
public PageResult<ItemDto> Get(ODataQueryOptions<Item> query)
{
    var results = (query.ApplyTo(db.Items, new ODataQuerySettings { PageSize = 20 }) as IEnumerable<Item>).Select(i => i.Map<ItemDto>());

    return new PageResult<ItemDto>(results, Request.GetNextPageLink(), Request.GetInlineCount());
}
I then try to filter items where any Tag.Name equals 'csharp':
/api/items?$filter=Tags/any(tag: tag/Name eq 'csharp')

However I then get the following error:
__Cannot compare elements of type 'System.Data.Objects.DataClasses.EntityCollection`1'. Only primitive types, enumeration types and entity types are supported.__

Full stack trace in case it's useful:
{"message":"An error has occurred.","exceptionmessage":"Exception has been thrown by the target of an invocation.","exceptiontype":"System.Reflection.TargetInvocationException","stacktrace":"   at System.Web.Http.ApiController.<InvokeActionWithExceptionFilters>d__a.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\r\n   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n   at System.Web.Http.Dispatcher.HttpControllerDispatcher.<SendAsync>d__0.MoveNext()","innerexception":{"message":"An error has occurred.","exceptionmessage":"Cannot compare elements of type 'System.Data.Objects.DataClasses.EntityCollection`1'. Only primitive types, enumeration types and entity types are supported.","exceptiontype":"System.NotSupportedException","stacktrace":"   at System.Data.Objects.ELinq.ExpressionConverter.VerifyTypeSupportedForComparison(Type clrType, TypeUsage edmType, Stack`1 memberPath)\r\n   at System.Data.Objects.ELinq.ExpressionConverter.CreateIsNullExpression(DbExpression operand, Type operandClrType)\r\n   at System.Data.Objects.ELinq.ExpressionConverter.EqualsTranslator.CreateIsNullExpression(ExpressionConverter parent, Expression input)\r\n   at System.Data.Objects.ELinq.ExpressionConverter.EqualsTranslator.TypedTranslate(ExpressionConverter parent, BinaryExpression linq)\r\n   at System.Data.Objects.ELinq.ExpressionConverter.TypedTranslator`1.Translate(ExpressionConverter parent, Expression linq)\r\n   at System.Data.Objects.ELinq.ExpressionConverter.TranslateExpression(Expression linq)\r\n   at System.Data.Objects.ELinq.ExpressionConverter.ConditionalTranslator.TypedTranslate(ExpressionConverter parent, ConditionalExpression linq)\r\n   at System.Data.Objects.ELinq.ExpressionConverter.TypedTranslator`1.Translate(ExpressionConverter parent, Expression linq)\r\n   at System.Data.Objects.ELinq.ExpressionConverter.TranslateExpression(Expression linq)\r\n   at System.Data.Objects.ELinq.ExpressionConverter.EqualsTranslator.TypedTranslate(ExpressionConverter parent, BinaryExpression linq)\r\n   at System.Data.Objects.ELinq.ExpressionConverter.TypedTranslator`1.Translate(ExpressionConverter parent, Expression linq)\r\n   at System.Data.Objects.ELinq.ExpressionConverter.TranslateExpression(Expression linq)\r\n   at System.Data.Objects.ELinq.ExpressionConverter.TranslateLambda(LambdaExpression lambda, DbExpression input)\r\n   at System.Data.Objects.ELinq.ExpressionConverter.TranslateLambda(LambdaExpression lambda, DbExpression input, DbExpressionBinding& binding)\r\n   at System.Data.Objects.ELinq.ExpressionConverter.MethodCallTranslator.OneLambdaTranslator.Translate(ExpressionConverter parent, MethodCallExpression call, DbExpression& source, DbExpressionBinding& sourceBinding, DbExpression& lambda)\r\n   at System.Data.Objects.ELinq.ExpressionConverter.MethodCallTranslator.OneLambdaTranslator.Translate(ExpressionConverter parent, MethodCallExpression call)\r\n   at System.Data.Objects.ELinq.ExpressionConverter.MethodCallTranslator.SequenceMethodTranslator.Translate(ExpressionConverter parent, MethodCallExpression call, SequenceMethod sequenceMethod)\r\n   at System.Data.Objects.ELinq.ExpressionConverter.MethodCallTranslator.TypedTranslate(ExpressionConverter parent, MethodCallExpression linq)\r\n   at System.Data.Objects.ELinq.ExpressionConverter.TypedTranslator`1.Translate(ExpressionConverter parent, Expression linq)\r\n   at System.Data.Objects.ELinq.ExpressionConverter.TranslateExpression(Expression linq)\r\n   at System.Data.Objects.ELinq.ExpressionConverter.MethodCallTranslator.OneLambdaTranslator.Translate(ExpressionConverter parent, MethodCallExpression call, DbExpression& source, DbExpressionBinding& sourceBinding, DbExpression& lambda)\r\n   at System.Data.Objects.ELinq.ExpressionConverter.MethodCallTranslator.OneLambdaTranslator.Translate(ExpressionConverter parent, MethodCallExpression call)\r\n   at System.Data.Objects.ELinq.ExpressionConverter.MethodCallTranslator.SequenceMethodTranslator.Translate(ExpressionConverter parent, MethodCallExpression call, SequenceMethod sequenceMethod)\r\n   at System.Data.Objects.ELinq.ExpressionConverter.MethodCallTranslator.TypedTranslate(ExpressionConverter parent, MethodCallExpression linq)\r\n   at System.Data.Objects.ELinq.ExpressionConverter.TypedTranslator`1.Translate(ExpressionConverter parent, Expression linq)\r\n   at System.Data.Objects.ELinq.ExpressionConverter.TranslateExpression(Expression linq)\r\n   at System.Data.Objects.ELinq.ExpressionConverter.MethodCallTranslator.UnarySequenceMethodTranslator.Translate(ExpressionConverter parent, MethodCallExpression call)\r\n   at System.Data.Objects.ELinq.ExpressionConverter.MethodCallTranslator.SequenceMethodTranslator.Translate(ExpressionConverter parent, MethodCallExpression call, SequenceMethod sequenceMethod)\r\n   at System.Data.Objects.ELinq.ExpressionConverter.MethodCallTranslator.TypedTranslate(ExpressionConverter parent, MethodCallExpression linq)\r\n   at System.Data.Objects.ELinq.ExpressionConverter.TypedTranslator`1.Translate(ExpressionConverter parent, Expression linq)\r\n   at System.Data.Objects.ELinq.ExpressionConverter.TranslateExpression(Expression linq)\r\n   at System.Data.Objects.ELinq.ExpressionConverter.Convert()\r\n   at System.Data.Objects.ELinq.ELinqQueryState.GetExecutionPlan(Nullable`1 forMergeOption)\r\n   at System.Data.Objects.ObjectQuery`1.GetResults(Nullable`1 forMergeOption)\r\n   at System.Data.Objects.ObjectQuery`1.System.Collections.Generic.IEnumerable<T>.GetEnumerator()\r\n   at System.Web.Http.OData.Query.ODataQueryOptions.LimitResults[T](IQueryable`1 queryable, Int32 limit, Boolean& resultsLimited)"}}
Apr 17, 2013 at 1:12 AM
Looks like EF is failing while translating the LINQ expression generated. Is it possible to share the Expression of the IQueryable? If you switch you call stack to this function call,

System.Web.Http.OData.Query.ODataQueryOptions.LimitResults[T](IQueryable`1 queryable, Int32 limit, Boolean& resultsLimited)

you should be able to see the expression on the queryable variable.
Apr 17, 2013 at 7:06 PM
Is there any way of doing this without having to run from source and redoing all the references in my web app? What do you suggest?
Apr 17, 2013 at 8:18 PM
I am not sure but if you are lucky enough the clr JIT might not have optimized these calls. You can disable "Just my code" to make this show up in the stack trace.
Apr 17, 2013 at 8:20 PM
Right went that route first, loaded all the debug symbols but when I tried viewing within that function nothing was visible due to "code optimizations."

I'm still trying but it's kind of a nightmare...
Apr 17, 2013 at 8:34 PM
BTW, if it is easier for you to share a repro, that would help too and is much appreciated. No need to fight with symbols and VS.
Apr 17, 2013 at 9:04 PM
Phew.

Ok, I should be able to get you most anything you need now.

queryable.Expression
{Convert(value(System.Data.Objects.ObjectSet`1[MyNamespace.Models.Item])).MergeAs(AppendOnly).Where($it => (IIF(($it.Tags == null), null, Convert($it.Tags.Any(tag => (IIF((tag == null), null, tag.Name) == value(System.Web.Http.OData.Query.Expressions.LinqParameterContainer+TypedLinqParameterContainer`1[System.String]).TypedProperty)))) == True)).OrderBy($it => $it.Id).Take(21)}
queryable.Expression.Arguments[0].DebugView
.Call System.Linq.Queryable.OrderBy(
    .Call System.Linq.Queryable.Where(
        .Call ((System.Data.Objects.ObjectQuery`1[MyNamespace.Models.Item]).Constant<System.Data.Objects.ObjectSet`1[MyNamespace.Models.Item]>(System.Data.Objects.ObjectSet`1[MyNamespace.Models.Item])).MergeAs(.Constant<System.Data.Objects.MergeOption>(AppendOnly))
        ,
        '(.Lambda #Lambda1<System.Func`2[MyNamespace.Models.Item,System.Boolean]>)),
    '(.Lambda #Lambda2<System.Func`2[MyNamespace.Models.Item,System.Int32]>))

.Lambda #Lambda1<System.Func`2[MyNamespace.Models.Item,System.Boolean]>(MyNamespace.Models.Item $$it) {
    .If ($$it.Tags == null) {
        null
    } .Else {
        (System.Nullable`1[System.Boolean]).Call System.Linq.Enumerable.Any(
            $$it.Tags,
            .Lambda #Lambda3<System.Func`2[MyNamespace.Models.Tag,System.Boolean]>)
    } == .Constant<System.Nullable`1[System.Boolean]>(True)
}

.Lambda #Lambda2<System.Func`2[MyNamespace.Models.Item,System.Int32]>(MyNamespace.Models.Item $$it) {
    $$it.Id
}

.Lambda #Lambda3<System.Func`2[MyNamespace.Models.Tag,System.Boolean]>(MyNamespace.Models.Tag $tag) {
    .If ($tag == null) {
        null
    } .Else {
        $tag.Name
    } == .Constant<System.Web.Http.OData.Query.Expressions.LinqParameterContainer+TypedLinqParameterContainer`1[System.String]>(System.Web.Http.OData.Query.Expressions.LinqParameterContainer+TypedLinqParameterContainer`1[System.String]).TypedProperty
}
Apr 17, 2013 at 9:14 PM
That is interesting. Looks like somehow null propagation is enabled for you even though you seem to be using EntityFramework. You should be able to disable it explicitly as EF handles (underlying database) null propagation for you.

Try disabling null propagation in the QuerySettings by doing this instead,
// GET api/items
public PageResult<ItemDto> Get(ODataQueryOptions<Item> query)
{
    var results = (query.ApplyTo(db.Items, new ODataQuerySettings { PageSize = 20,  HandleNullPropagation = HandleNullPropagationOption.False }) as IEnumerable<Item>).Select(i => i.Map<ItemDto>());

    return new PageResult<ItemDto>(results, Request.GetNextPageLink(), Request.GetInlineCount());
}
Apr 17, 2013 at 9:21 PM
Thanks, that did it!

Is there anything else I can provide to help get to the root cause, while I have this complete debugging setup?
Apr 17, 2013 at 9:22 PM
I realized why HandleNullPropagationOption was true even though you are using EntityFramework. It is because you are not using code first and hence your context is not DbContext. It is ObjectContext.

I have opened this issue to fix this https://aspnetwebstack.codeplex.com/workitem/995
Apr 17, 2013 at 9:22 PM
aforty wrote:
Thanks, that did it!

Is there anything else I can provide to help get to the root cause, while I have this complete debugging setup?
Thanks a lot for helping me with the debugging.
Apr 17, 2013 at 9:23 PM
No problem! That also wouldn't be the first time model-first has come to bite me in the ass.
Apr 17, 2013 at 9:39 PM
aforty wrote:
Also, side question: is there any good blog, text file (... or anything!) to keep up with changes made to Odata/WebAPI on a nightly/weekly/monthly basis. I find OData to be ever changing yet very poorly documented.
BTW, regarding your side question from the original post, we have started putting our design specs and meeting notes as well on our public wiki. That would be a good place to catch-up on the latest features.

The links -
https://aspnetwebstack.codeplex.com/wikipage?title=Specs
https://aspnetwebstack.codeplex.com/wikipage?title=Meeting%20Notes

You should be able to find samples in the specs. And if you happen to find some useful feature there and don't like the design, please comment so that we can fix it.
Apr 17, 2013 at 9:59 PM
Very cool, will do!