Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IN operator on an ENUM value causes an exception #1375

Open
ThomasHeijtink opened this issue Dec 16, 2024 · 4 comments
Open

IN operator on an ENUM value causes an exception #1375

ThomasHeijtink opened this issue Dec 16, 2024 · 4 comments
Assignees
Labels
bug Something isn't working

Comments

@ThomasHeijtink
Copy link

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)
@ThomasHeijtink ThomasHeijtink added the bug Something isn't working label Dec 16, 2024
@corranrogue9
Copy link
Contributor

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.

@julealgon
Copy link
Contributor

@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.

@corranrogue9
Copy link
Contributor

@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 corranrogue9 self-assigned this Dec 17, 2024
@ThomasHeijtink
Copy link
Author

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

3 participants