Webapi: The IN operator not working with Enum

Created on 9 Nov 2018  路  12Comments  路  Source: OData/WebApi

When filtering values of Enum by the IN operator, api returns 500 (Internal Server Error)..

Assemblies affected

Microsoft.AspNet.OData 7.0.1

Reproduce steps

   public class User
    {
        public long Id { get; set; }
        public string Name { get; set; }
        public UserRole Role { get; set; }
    }

    public enum UserRole
    {
        Administrator,
        Manager,
    }

Request: http://localhost:63000/v1/Users?$filter=Role in ('Administrator')

Expected result

{
    "@odata.context": "http://localhost:63000/v1/$metadata#Users",
    "value": [
        {
            "Id": 1,
            "Name": "User_1",
            "Role": "Administrator"
        }
    ]
}

Actual result

{
    "error": {
        "code": "",
        "message": "An error has occurred.",
        "innererror": {
            "message": "The value \"Microsoft.OData.ODataEnumValue\" is not of type \"ODataExample.Model.UserRole\" and cannot be used in this generic collection.\r\nParameter name: value",
            "type": "System.ArgumentException",
            "stacktrace": "   at System.ThrowHelper.ThrowWrongValueTypeArgumentException(Object value, Type targetType)\r\n   at System.Collections.Generic.List`1.System.Collections.IList.Add(Object item)\r\n   at Microsoft.AspNet.OData.Query.Expressions.FilterBinder.BindCollectionConstantNode(CollectionConstantNode node)\r\n   at Microsoft.AspNet.OData.Query.Expressions.FilterBinder.BindCollectionNode(CollectionNode node)\r\n   at Microsoft.AspNet.OData.Query.Expressions.FilterBinder.Bind(QueryNode node)\r\n   at Microsoft.AspNet.OData.Query.Expressions.FilterBinder.BindInNode(InNode inNode)\r\n   at Microsoft.AspNet.OData.Query.Expressions.FilterBinder.BindSingleValueNode(SingleValueNode node)\r\n   at Microsoft.AspNet.OData.Query.Expressions.FilterBinder.Bind(QueryNode node)\r\n   at Microsoft.AspNet.OData.Query.Expressions.FilterBinder.BindExpression(SingleValueNode expression, RangeVariable rangeVariable, Type elementType)\r\n   at Microsoft.AspNet.OData.Query.Expressions.FilterBinder.BindFilterClause(FilterBinder binder, FilterClause filterClause, Type filterType)\r\n   at Microsoft.AspNet.OData.Query.Expressions.FilterBinder.Bind(IQueryable baseQuery, FilterClause filterClause, Type filterType, ODataQueryContext context, ODataQuerySettings querySettings)\r\n   at Microsoft.AspNet.OData.Query.FilterQueryOption.ApplyTo(IQueryable query, ODataQuerySettings querySettings)\r\n   at Microsoft.AspNet.OData.Query.ODataQueryOptions.ApplyTo(IQueryable query, ODataQuerySettings querySettings)\r\n   at Microsoft.AspNet.OData.EnableQueryAttribute.ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)\r\n   at Microsoft.AspNet.OData.EnableQueryAttribute.ExecuteQuery(Object responseValue, IQueryable singleResultCollection, IWebApiActionDescriptor actionDescriptor, Func`2 modelFunction, IWebApiRequestMessage request, Func`2 createQueryOptionFunction)\r\n   at Microsoft.AspNet.OData.EnableQueryAttribute.OnActionExecuted(Object responseValue, IQueryable singleResultCollection, IWebApiActionDescriptor actionDescriptor, IWebApiRequestMessage request, Func`2 modelFunction, Func`2 createQueryOptionFunction, Action`1 createResponseAction, Action`3 createErrorAction)\r\n   at Microsoft.AspNet.OData.EnableQueryAttribute.OnActionExecuted(HttpActionExecutedContext actionExecutedContext)\r\n   at System.Web.Http.Filters.ActionFilterAttribute.OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken)\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.Runtime.CompilerServices.TaskAwaiter.GetResult()\r\n   at System.Web.Http.Filters.ActionFilterAttribute.<CallOnActionExecutedAsync>d__5.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.Runtime.CompilerServices.TaskAwaiter`1.GetResult()\r\n   at System.Web.Http.Filters.ActionFilterAttribute.<ExecuteActionFilterAsyncCore>d__0.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.Runtime.CompilerServices.TaskAwaiter`1.GetResult()\r\n   at System.Web.Http.Controllers.ActionFilterResult.<ExecuteAsync>d__2.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.Runtime.CompilerServices.TaskAwaiter`1.GetResult()\r\n   at System.Web.Http.Dispatcher.HttpControllerDispatcher.<SendAsync>d__1.MoveNext()"
        }
    }
}

Additional detail

Example: https://github.com/flibustier7seas/odata-example#the-in-operator-not-working-with-enum

investigating

Most helpful comment

I just opened a PR which should fix the issue. Sorry to state it that bad but the way how the OData project is maintained sometimes is really poor. Communication is not happening and even when stuff is put to "investigating" or "ready-for-review" nothing happens. It was literaly 1 line of code to be changed and it took me a few minutes to get it fixed.

All 12 comments

Any updates?

Still broken in 7.2 :(

@mburbea It is likely that this is still unfixed as it was not prioritized for the set of fixes for this release. I can work with you if you are willing to contribute a fix.
There were some IN operator improvements in ODL in some previous releases if that may have fixed this. Do you know what ODL version you are using?

I'm using Microsoft.OData.Core 7.6 when I tried just now.
As a workaround, my webapp is translating expressions that should be in to or. (e.g.
color eq 'Red' or color eq 'Blue' or color eq 'Green', as opposed to the simpler color in ('Red','Blue','Green')

I quickly debugged the library and the issue is in the FilterBinder.BindCollectionConstantNode:

https://github.com/OData/WebApi/blob/7ab7dbf5e7f814c19f140e27404061036dff49fa/src/Microsoft.AspNet.OData.Shared/Query/Expressions/FilterBinder.cs#L384-L405

The method creates a new List<EnumType> via reflection, but the ContantNode item contains a ODataEnumValue. For other queries like with integers like $filter=id in (1,2,3) the RetrieveClrTypeForConstant properly unwraps the value for the enum but the collection adding does not. I will locally try to prepare a fix and let you know

Edit: I was able to fix it with a very simple code change.

Line 402 simply needs to be changed to:

castedList.Add(EnumDeserializationHelpers.ConvertEnumValue(item.Value, constantType));

I just opened a PR which should fix the issue. Sorry to state it that bad but the way how the OData project is maintained sometimes is really poor. Communication is not happening and even when stuff is put to "investigating" or "ready-for-review" nothing happens. It was literaly 1 line of code to be changed and it took me a few minutes to get it fixed.

The fix, which I assume in in 7.2.2, doesnt work when your enum is nullable on the model. Shall I raise another issue?

Model:

public class Product
  {
    [Key]
    public int PK { get; set; }
    public string Name { get; set; }
    public MyEnum? Optional { get; set; }
    public MyEnum Required { get; set; }
  }

  public enum MyEnum
  {
    NotSpecified,
    A,
    B,
    C
  }

Products?$filter=(Required in ('A', 'B')) succeeds.

Products?$filter=(Optional in ('A', 'B')) fails with error:

{
    "error": {
        "code": "",
        "message": "An error has occurred.",
        "innererror": {
            "message": "Expression of type 'System.Collections.Generic.List`1[ODataService.Models.MyEnum]' cannot be used for parameter of type 'System.Collections.Generic.IEnumerable`1[System.Nullable`1[ODataService.Models.MyEnum]]' of method 'Boolean Contains[Nullable`1](System.Collections.Generic.IEnumerable`1[System.Nullable`1[ODataService.Models.MyEnum]], System.Nullable`1[ODataService.Models.MyEnum])'",
            "type": "System.ArgumentException",
            "stacktrace": "   at System.Linq.Expressions.Expression.ValidateOneArgument(MethodBase method, ExpressionType nodeKind, Expression arg, ParameterInfo pi)\r\n   at System.Linq.Expressions.Expression.Call(Expression instance, MethodInfo method, Expression arg0, Expression arg1)\r\n   at Microsoft.AspNet.OData.Query.Expressions.FilterBinder.BindInNode(InNode inNode)\r\n   at Microsoft.AspNet.OData.Query.Expressions.FilterBinder.BindSingleValueNode(SingleValueNode node)\r\n   at Microsoft.AspNet.OData.Query.Expressions.FilterBinder.Bind(QueryNode node)\r\n   at Microsoft.AspNet.OData.Query.Expressions.FilterBinder.BindExpression(SingleValueNode expression, RangeVariable rangeVariable, Type elementType)\r\n   at Microsoft.AspNet.OData.Query.Expressions.FilterBinder.BindFilterClause(FilterBinder binder, FilterClause filterClause, Type filterType)\r\n   at Microsoft.AspNet.OData.Query.Expressions.FilterBinder.Bind(IQueryable baseQuery, FilterClause filterClause, Type filterType, ODataQueryContext context, ODataQuerySettings querySettings)\r\n   at Microsoft.AspNet.OData.Query.FilterQueryOption.ApplyTo(IQueryable query, ODataQuerySettings querySettings)\r\n   at Microsoft.AspNet.OData.Query.ODataQueryOptions.ApplyTo(IQueryable query, ODataQuerySettings querySettings)\r\n   at Microsoft.AspNet.OData.EnableQueryAttribute.ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)\r\n   at Microsoft.AspNet.OData.EnableQueryAttribute.ExecuteQuery(Object responseValue, IQueryable singleResultCollection, IWebApiActionDescriptor actionDescriptor, Func`2 modelFunction, IWebApiRequestMessage request, Func`2 createQueryOptionFunction)\r\n   at Microsoft.AspNet.OData.EnableQueryAttribute.OnActionExecuted(Object responseValue, IQueryable singleResultCollection, IWebApiActionDescriptor actionDescriptor, IWebApiRequestMessage request, Func`2 modelFunction, Func`2 createQueryOptionFunction, Action`1 createResponseAction, Action`3 createErrorAction)\r\n   at Microsoft.AspNet.OData.EnableQueryAttribute.OnActionExecuted(HttpActionExecutedContext actionExecutedContext)\r\n   at System.Web.Http.Filters.ActionFilterAttribute.OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken)\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.Filters.ActionFilterAttribute.<CallOnActionExecutedAsync>d__6.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.Filters.ActionFilterAttribute.<ExecuteActionFilterAsyncCore>d__5.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.Controllers.ActionFilterResult.<ExecuteAsync>d__5.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__15.MoveNext()"
        }
    }
}

Example repo: https://github.com/blueghostuk/ODataService

The bug seems to be related to the same area of code that was touched for the initial fix. I would see it to be fixed as part of this issue.

The nullable type is always unwrapped for parsing the value. It would currently also fail for scenarios where you want to check if the enum value is null like Products?$filter=(Optional in ('A', null)).

https://github.com/OData/WebApi/blob/5061dcad757599b93ff990079832123a73e0e091/src/Microsoft.AspNet.OData.Shared/Query/Expressions/FilterBinder.cs#L1589

I tried to prepare a fix for it and this particular case is now handled. But my proposed change might have bigger impact and needs better handling. Let's see what the official members say.

Do you know how I can make it work when using the integer values of the enum, for example:

http://localhost:63000/v1/Users?$filter=Role in (5, 7)

For now it seems to only work with the string values of the enum.

Edit: surrounding the integer values with single quotes appears to work:

http://localhost:63000/v1/Users?$filter=Role in ('5', '7')

Isn't "in" supposed to use brackets not parenthesis?

Isn't "in" supposed to use brackets not parenthesis?
https://github.com/OData/odata.net/issues/1385

Merged. Close it.

Was this page helpful?
0 / 5 - 0 ratings