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

Array of Enums not working for Filtering #536

Closed
SantoshPabba opened this issue Mar 23, 2022 · 11 comments
Closed

Array of Enums not working for Filtering #536

SantoshPabba opened this issue Mar 23, 2022 · 11 comments

Comments

@SantoshPabba
Copy link

Hi Everyone,

I was trying to filter the result using the OData filter, in which one of the columns contains a List of Enums (i.e. Days of the week). But it is throwing the OData error like below.

response JSON example :

"name": "Shift 1", "status": "Approved", "workingDays": [ "Monday", "Tuesday", "Wednesday", "Thursday", "Friday" ], "id": 1

url: odata/myshifts?$filter=workingDays/any(t:contains(t,'Monday'))

Resonse
"message": "No function signature for the function with name 'contains' matches the specified arguments. The function signatures considered are: contains(Edm.String Nullable=true, Edm.String Nullable=true)."

{
    "error": {
        "code": "",
        "message": "The query specified in the URI is not valid. No function signature for the function with name 'contains' matches the specified arguments. The function signatures considered are: contains(Edm.String Nullable=true, Edm.String Nullable=true).",
        "details": [],
        "innererror": {
            "message": "No function signature for the function with name 'contains' matches the specified arguments. The function signatures considered are: contains(Edm.String Nullable=true, Edm.String Nullable=true).",
            "type": "Microsoft.OData.ODataException",
            "stacktrace": "   at Microsoft.OData.UriParser.FunctionCallBinder.MatchSignatureToUriFunction(String functionCallToken, SingleValueNode[] argumentNodes, IList`1 nameSignatures)\n   at Microsoft.OData.UriParser.FunctionCallBinder.BindAsUriFunction(FunctionCallToken functionCallToken, List`1 argumentNodes)\n   at Microsoft.OData.UriParser.FunctionCallBinder.BindFunctionCall(FunctionCallToken functionCallToken)\n   at Microsoft.OData.UriParser.MetadataBinder.BindFunctionCall(FunctionCallToken functionCallToken)\n   at Microsoft.OData.UriParser.MetadataBinder.Bind(QueryToken token)\n   at Microsoft.OData.UriParser.LambdaBinder.BindExpressionToken(QueryToken queryToken)\n   at Microsoft.OData.UriParser.LambdaBinder.BindLambdaToken(LambdaToken lambdaToken, BindingState state)\n   at Microsoft.OData.UriParser.MetadataBinder.BindAnyAll(LambdaToken lambdaToken)\n   at Microsoft.OData.UriParser.MetadataBinder.Bind(QueryToken token)\n   at Microsoft.OData.UriParser.FilterBinder.BindFilter(QueryToken filter)\n   at Microsoft.OData.UriParser.ODataQueryOptionParser.ParseFilterImplementation(String filter, ODataUriParserConfiguration configuration, ODataPathInfo odataPathInfo)\n   at Microsoft.OData.UriParser.ODataQueryOptionParser.ParseFilter()\n   at Microsoft.AspNetCore.OData.Query.FilterQueryOption.get_FilterClause()\n   at Microsoft.AspNetCore.OData.Query.Validator.FilterQueryValidator.Validate(FilterQueryOption filterQueryOption, ODataValidationSettings settings)\n   at Microsoft.AspNetCore.OData.Query.FilterQueryOption.Validate(ODataValidationSettings validationSettings)\n   at Microsoft.AspNetCore.OData.Query.Validator.ODataQueryValidator.Validate(ODataQueryOptions options, ODataValidationSettings validationSettings)\n   at Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.ValidateQuery(HttpRequest request, ODataQueryOptions queryOptions)\n   at Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.OnActionExecuting(ActionExecutingContext actionExecutingContext)"
        }
    }
}
@julealgon
Copy link
Contributor

Isn't that because the function only accepts strings and not enums?

@dweeden
Copy link

dweeden commented Mar 23, 2022

PR #439 apparently fixes it but is pending on a test case being created.

@xuzhg
Copy link
Member

xuzhg commented Mar 23, 2022

@SantoshPabba Does a cast function call work for you?

workingDays/any(t:contains(cast(t, Edm.String),'Monday')) or

workingDays/any(t:contains(cast(t, 'Edm.String'),'Monday'))

By the way, what do you want to return? Return myshifts only its workingDays contains 'Monday'?

if yes, why don't use 'eq'? as odata/myshifts?$filter=workingDays/any(t:t eq 'Monday'))

or you also can use 'in' operator: odata/myshifts?$filter='Monday' in workingDays

I haven't found a chance to test the aboves. Let me know if none of them doesn't work.

@SantoshPabba
Copy link
Author

Isn't that because the function only accepts strings and not enums?

Hi @dweeden,

I have already used StringAsEnumResolver in my code to show enums as Text rather numbers

In my case, I have an array of enums in one property and trying to filter the result based on 1 value that matches the query.

I am looking for a Shifts endpoint, which contains "Monday" as one of the working days. It should return all days pertaining to a shift ID, but the day should match the filter.

@SantoshPabba
Copy link
Author

SantoshPabba commented Mar 24, 2022

@SantoshPabba Does a cast function call work for you?

workingDays/any(t:contains(cast(t, Edm.String),'Monday')) or

workingDays/any(t:contains(cast(t, 'Edm.String'),'Monday'))

By the way, what do you want to return? Return myshifts only its workingDays contains 'Monday'?

if yes, why don't use 'eq'? as odata/myshifts?$filter=workingDays/any(t:t eq 'Monday'))

or you also can use 'in' operator: odata/myshifts?$filter='Monday' in workingDays

I haven't found a chance to test the aboves. Let me know if none of them doesn't work.

Hi @xuzhg ,

Thanks for the reply, I tried the first 2 queries but got an empty result and the next two were throwing 500 errors.

@habbes
Copy link
Contributor

habbes commented Mar 29, 2022

@xuzhg @SantoshPabba I have tried the options as well.

The first two options proposed by @xuzhg, where there's an explicit cast to Edm.String, return an empty array result, as @SantoshPabba had pointed out.

$filter='Monday' in WorkingDays threw the following exception saying that the Edm.String and WorkingDay cannot be compared

System.ArgumentException: An instance of InNode can only be created where the item types of the right operand 'ODataRoutingSample.Models.WorkingDay' and the left operand 'Edm.String' can be compared.

However, when I used the fully qualified enum as follows, it returns the correct result:

$filter=ODataRoutingSample.Models.WorkingDay'Monday' in WorkingDays

In the example above, OdataRoutingSample.Models is the namespace.

Also, when I use any and eq without casting it also returns the correct result on my end. I am not sure why it did not work for @SantoshPabba:

$filter=WorkingDays/any(t:t eq 'Monday')

returned:

{
    "@odata.context": "http://localhost:64771/v1/$metadata#Shifts",
    "value": [
        {
            "Id": 1,
            "Name": "Shift 1",
            "WorkingDays": [
                "Monday",
                "Tuesday",
                "Thursday"
            ]
        },
        {
            "Id": 3,
            "Name": "Shift 3",
            "WorkingDays": [
                "Monday",
                "Friday",
                "Thursday"
            ]
        }
    ]
}

@SantoshPabba could you share the error message you got?

@xuzhg is the fact that in operator does not support enums without the namespace prefix a bug, feature gap or expected behaviour? According to the spec :

  Enumeration literals in OData 4.0 required prefixing with the qualified type name of the enumeration.

  In OData 4.01, services MUST support duration and enumeration literals with or without the type prefix. OData clients that 
  want to operate across OData 4.0 and OData 4.01 services should always include the prefix for duration and enumeration types.

@habbes
Copy link
Contributor

habbes commented Mar 30, 2022

@SantoshPabba @xuzhg

I've found that the reason there's an error when using 'Monday' in WorkingDays but WorkingDays/anyt:t eq 'Monday' is due to the difference in how the URI parser handles the in operator vs primitive binary operators like eq.

For binary operators, the BinaryOperatorBinder promotes the operands to compatible types, specifically, strings are promoted to enums where applicable:

internal QueryNode BindBinaryOperator(BinaryOperatorToken binaryOperatorToken)
{
    ExceptionUtils.CheckArgumentNotNull(binaryOperatorToken, "binaryOperatorToken");

    SingleValueNode left = this.GetOperandFromToken(binaryOperatorToken.OperatorKind, binaryOperatorToken.Left);
    SingleValueNode right = this.GetOperandFromToken(binaryOperatorToken.OperatorKind, binaryOperatorToken.Right);

    IEdmTypeReference typeReference;
    this.resolver.PromoteBinaryOperandTypes(binaryOperatorToken.OperatorKind, ref left, ref right, out typeReference);

    return new BinaryOperatorNode(binaryOperatorToken.OperatorKind, left, right, typeReference);
}

The InBinder does not promote types. The InNode throws an exception if the type of one operand is not assignable to the other.

Maybe we should report this a bug or feature gap in ODL and have it fixed there.

@xuzhg
Copy link
Member

xuzhg commented Mar 30, 2022

@habbes Yes. It's a gap/bug as I mentioned that EnumAsStringResolver is not applied to In operator parser. Please file an issue in the ODL side to track on it.

@habbes
Copy link
Contributor

habbes commented Mar 30, 2022

@SantoshPabba another approach you could consider is to store the enums as flags instead of a collection of enums and use the has operator to check whether a particular working day has been set in the bit field.

Here's how you would define the enum:

[Flags]
public enum WorkingDay
{
   None = 0;
   Monday = 1;
   Tuesday = 2;
   Wednesday = 4;
   Thursday = 8;
   Friday = 16;
   // etc. keep doubling to ensure each value has its own bit position
}

Then in your entity, define a single-value field instead of a collection:

public class Shift
{
  public int Id {get; set;}
  public int Name {get; set;}
  public WorkingDay WorkingDays {get;set;}
  //...
}

And here's how you would set the field values:

Shift shift = new Shift
{
   Name = "Shift 1",
   WorkingDays = WorkingDay.Monday | WorkingDay.Wednesday | WorkingDay.Friday
   // ...
}

And here's what your query would look like

odata/myshifts?$filter=WorkingDays has 'Monday'

@SantoshPabba
Copy link
Author

Hi @habbes

Thanks for your response. I tried both the syntaxes you suggested and working as expected. I might miss something in the second syntax while trying for the first time ( @xuzhg suggestion).

Working Syntaxes:

$filter=ODataRoutingSample.Models.WorkingDay'Monday' in WorkingDays
$filter=WorkingDays/any(t:t eq 'Monday')

Thanks to @habbes, @xuzhg and others for your extended support in resolving my issue.

@julealgon
Copy link
Contributor

Here's how you would define the enum:

@habbes I'd suggest pluralizing your enum name in that sample, since having flag enums be plural is a best practice enforced by static analysis

I'd also probably recommend either using binary literals or bit shifted left ints as the values for maintainability, but that one is a lot more subjective.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants