AspNetCoreOData
AspNetCoreOData copied to clipboard
IN operator on an ENUM value causes an exception
Assemblies affected Which assemblies and versions are known to be affected e.g. ASP.NET Core OData 8.x ASP.NET Core OData 9.1.1
Describe the bug
Filtering on an Enum property using the integer value as string in combination with the IN operator causes the following exception:
Microsoft.OData.ODataException: The string '3' is not a valid enumeration type constant.
When filtering on an enum value using just an integer value in combination with the IN operator we get the following exception:
Microsoft.OData.ODataException: Cannot read the value '3' as a quoted JSON string value.
Regular filtering using the EQ operator on any of these two representation of an enum value works fine.
Reproduce steps
Take the EnumsController. Change the Get to using the ODataQueryOptions<Employee> directly (rather than using the EnableQuery attribute) and issue a GET to http://localhost:5000/convention/employees?$filter=Gender in (2,3) or http://localhost:5000/convention/employees?$filter=Gender in ('2','3').
Data Model
Employee model in the ODataCustomizedSample sample project.
EDM (CSDL) Model Not applicable
Request/Response Not relevant
Expected behavior The endpoint to return employees of either gender.
Screenshots Not applicable
Additional context Exception + stacktrace with quoted numerical enum value:
Microsoft.OData.ODataException: The string '2' is not a valid enumeration type constant.
at Microsoft.OData.UriParser.MetadataBindingUtils.VerifyCollectionNode(CollectionNode node, Boolean enableCaseInsensitive)
at Microsoft.OData.UriParser.InBinder.BindInOperator(InToken inToken, BindingState state)
at Microsoft.OData.UriParser.MetadataBinder.BindIn(InToken inToken)
at Microsoft.OData.UriParser.MetadataBinder.Bind(QueryToken token)
at Microsoft.OData.UriParser.FilterBinder.BindFilter(QueryToken filter)
at Microsoft.OData.UriParser.ODataQueryOptionParser.ParseFilterImplementation(String filter, ODataUriParserConfiguration configuration, ODataPathInfo odataPathInfo)
at Microsoft.OData.UriParser.ODataQueryOptionParser.ParseFilter()
at Microsoft.AspNetCore.OData.Query.FilterQueryOption.get_FilterClause() in C:\AspNetCoreOData\src\Microsoft.AspNetCore.OData\Query\Query\FilterQueryOption.cs:line 118
at Microsoft.AspNetCore.OData.Query.FilterQueryOption.ApplyTo(IQueryable query, ODataQuerySettings querySettings) in C:\AspNetCoreOData\src\Microsoft.AspNetCore.OData\Query\Query\FilterQueryOption.cs:line 161
at Microsoft.AspNetCore.OData.Query.ODataQueryOptions.ApplyTo(IQueryable query, ODataQuerySettings querySettings) in C:\AspNetCoreOData\src\Microsoft.AspNetCore.OData\Query\ODataQueryOptions.cs:line 387
at Microsoft.AspNetCore.OData.Query.ODataQueryOptions`1.ApplyTo(IQueryable query, ODataQuerySettings querySettings) in C:\AspNetCoreOData\src\Microsoft.AspNetCore.OData\Query\ODataQueryOptionsOfT.cs:line 95
at Microsoft.AspNetCore.OData.Query.ODataQueryOptions.ApplyTo(IQueryable query) in C:\AspNetCoreOData\src\Microsoft.AspNetCore.OData\Query\ODataQueryOptions.cs:line 301
at Microsoft.AspNetCore.OData.Query.ODataQueryOptions`1.ApplyTo(IQueryable query) in C:\AspNetCoreOData\src\Microsoft.AspNetCore.OData\Query\ODataQueryOptionsOfT.cs:line 83
at ODataCustomizedSample.Controller.EmployeesController.Get(ODataQueryOptions`1 queryOptions) in C:\AspNetCoreOData\sample\ODataCustomizedSample\Controllers\EnumsController.cs:line 83
at lambda_method2(Closure, Object, Object[])
at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncActionResultExecutor.Execute(ActionContext actionContext, IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Logged|12_1(ControllerActionInvoker invoker)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.OData.Routing.ODataRouteDebugMiddleware.Invoke(HttpContext context) in C:\AspNetCoreOData\src\Microsoft.AspNetCore.OData\Routing\ODataRouteDebugMiddleware.cs:line 80
at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
Exception + stacktrace without quoted numerical enum value:
Microsoft.OData.ODataException: Cannot read the value '2' as a quoted JSON string value.
at Microsoft.OData.Json.JsonReaderExtensions.ReadStringValue(IJsonReader jsonReader)
at Microsoft.OData.Json.ODataJsonPropertyAndValueDeserializer.ReadEnumValue(Boolean insideJsonObjectValue, IEdmEnumTypeReference expectedValueTypeReference, Boolean validateNullValue, String propertyName)
at Microsoft.OData.Json.ODataJsonPropertyAndValueDeserializer.ReadNonEntityValueImplementation(String payloadTypeName, IEdmTypeReference expectedTypeReference, PropertyAndAnnotationCollector propertyAndAnnotationCollector, CollectionWithoutExpectedTypeValidator collectionValidator, Boolean validateNullValue, Boolean isTopLevelPropertyValue, Boolean insideResourceValue, String propertyName, Nullable`1 isDynamicProperty)
at Microsoft.OData.Json.ODataJsonPropertyAndValueDeserializer.ReadCollectionValue(IEdmCollectionTypeReference collectionValueTypeReference, String payloadTypeName, ODataTypeAnnotation typeAnnotation)
at Microsoft.OData.Json.ODataJsonPropertyAndValueDeserializer.ReadNonEntityValueImplementation(String payloadTypeName, IEdmTypeReference expectedTypeReference, PropertyAndAnnotationCollector propertyAndAnnotationCollector, CollectionWithoutExpectedTypeValidator collectionValidator, Boolean validateNullValue, Boolean isTopLevelPropertyValue, Boolean insideResourceValue, String propertyName, Nullable`1 isDynamicProperty)
at Microsoft.OData.Json.ODataJsonPropertyAndValueDeserializer.ReadNonEntityValue(String payloadTypeName, IEdmTypeReference expectedValueTypeReference, PropertyAndAnnotationCollector propertyAndAnnotationCollector, CollectionWithoutExpectedTypeValidator collectionValidator, Boolean validateNullValue, Boolean isTopLevelPropertyValue, Boolean insideResourceValue, String propertyName, Nullable`1 isDynamicProperty)
at Microsoft.OData.ODataUriConversionUtils.ConvertFromResourceOrCollectionValue(String value, IEdmModel model, IEdmTypeReference typeReference)
at Microsoft.OData.ODataUriConversionUtils.ConvertFromCollectionValue(String value, IEdmModel model, IEdmTypeReference typeReference)
at Microsoft.OData.UriParser.InBinder.GetCollectionOperandFromToken(QueryToken queryToken, IEdmTypeReference expectedType, IEdmModel model)
at Microsoft.OData.UriParser.InBinder.BindInOperator(InToken inToken, BindingState state)
at Microsoft.OData.UriParser.MetadataBinder.BindIn(InToken inToken)
at Microsoft.OData.UriParser.MetadataBinder.Bind(QueryToken token)
at Microsoft.OData.UriParser.FilterBinder.BindFilter(QueryToken filter)
at Microsoft.OData.UriParser.ODataQueryOptionParser.ParseFilterImplementation(String filter, ODataUriParserConfiguration configuration, ODataPathInfo odataPathInfo)
at Microsoft.OData.UriParser.ODataQueryOptionParser.ParseFilter()
at Microsoft.AspNetCore.OData.Query.FilterQueryOption.get_FilterClause() in C:\AspNetCoreOData\src\Microsoft.AspNetCore.OData\Query\Query\FilterQueryOption.cs:line 118
at Microsoft.AspNetCore.OData.Query.FilterQueryOption.ApplyTo(IQueryable query, ODataQuerySettings querySettings) in C:\AspNetCoreOData\src\Microsoft.AspNetCore.OData\Query\Query\FilterQueryOption.cs:line 161
at Microsoft.AspNetCore.OData.Query.ODataQueryOptions.ApplyTo(IQueryable query, ODataQuerySettings querySettings) in C:\AspNetCoreOData\src\Microsoft.AspNetCore.OData\Query\ODataQueryOptions.cs:line 387
at Microsoft.AspNetCore.OData.Query.ODataQueryOptions`1.ApplyTo(IQueryable query, ODataQuerySettings querySettings) in C:\AspNetCoreOData\src\Microsoft.AspNetCore.OData\Query\ODataQueryOptionsOfT.cs:line 95
at Microsoft.AspNetCore.OData.Query.ODataQueryOptions.ApplyTo(IQueryable query) in C:\AspNetCoreOData\src\Microsoft.AspNetCore.OData\Query\ODataQueryOptions.cs:line 301
at Microsoft.AspNetCore.OData.Query.ODataQueryOptions`1.ApplyTo(IQueryable query) in C:\AspNetCoreOData\src\Microsoft.AspNetCore.OData\Query\ODataQueryOptionsOfT.cs:line 83
at ODataCustomizedSample.Controller.EmployeesController.Get(ODataQueryOptions`1 queryOptions) in C:\AspNetCoreOData\sample\ODataCustomizedSample\Controllers\EnumsController.cs:line 83
at lambda_method2(Closure, Object, Object[])
at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncActionResultExecutor.Execute(ActionContext actionContext, IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Logged|12_1(ControllerActionInvoker invoker)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.OData.Routing.ODataRouteDebugMiddleware.Invoke(HttpContext context) in C:\Users\tomhe\RiderProjects\AspNetCoreOData\src\Microsoft.AspNetCore.OData\Routing\ODataRouteDebugMiddleware.cs:line 80
at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
The OData standard ABNF for the enum rule is:
enum = [ qualifiedEnumTypeName ] SQUOTE enumValue SQUOTE enumValue = singleEnumValue *( COMMA singleEnumValue ) singleEnumValue = enumerationMember / enumMemberValue enumMemberValue = int64Value
So I believe the "quoted" request http://localhost:5000/convention/employees?$filter=Gender in ('2','3') is the correct syntax. From your stack trace, it appears the offending line is here where we check only if there's an enum member name that matches the value provided in the filter expression.
We should update this logic to allow for enum member values if no matching enum member name is found to match.
@corranrogue9
From your stack trace, it appears the offending line is here where we check only if there's an enum member name that matches the value provided in the filter expression.
We should update this logic to allow for enum member values if no matching enum member name is found to match.
Is there a reason such code doesn't rely on something like Enum.TryParse? That would handle both text as well as numeric forms.
@ThomasHeijtink also, is there a reason that you are preferring to use the integer value for the enum rather than the member names?
@julealgon that sounds like the right approach, I'll double check that the standard doesn't have any quirk that prevents using it, but that's probably what I'll go with, thanks!
@corranrogue9 thanks for asking. It's mainly to keep queries small and slightly more robust and more versatile. Small is evident. Robust because a front-end or other clients don't require the most recent name/version in case we change it at the backend. Versatile because you can also use flagged enums.
@corranrogue9
So I believe the "quoted" request
http://localhost:5000/convention/employees?$filter=Gender in ('2','3')is the correct syntax.
Just wanted to note that when using the equality operator ODATA seems to accept unquoted integers as well. This might throw people off when using it with different operators like in. Personally, I would have preferred accepting it as pure integers. It seems to make most sense to me. For example, using the greater than or when aggregating enum values seem more intuitive. But I take any win I can get at this point 😉