Fix parsing issue for Flags enum values with combined flags#3324
Fix parsing issue for Flags enum values with combined flags#3324WanjohiSammy merged 89 commits intomainfrom
Conversation
|
/AzurePipelines run |
|
No pipelines are associated with this pull request. |
|
/AzurePipelines run |
|
No pipelines are associated with this pull request. |
8e03a95 to
2bfab4c
Compare
|
/AzurePipelines run |
|
No pipelines are associated with this pull request. |
|
/AzurePipelines run |
| { | ||
| // Sort members by descending flag value to prioritize composite flags | ||
| var members = enumType.Members | ||
| .OrderByDescending(m => m.Value.Value); |
There was a problem hiding this comment.
OrderBy is necessary? I cannot see any advantage to use the orderred members?
There was a problem hiding this comment.
@xuzhg Thanks for pointing this out.
The use of OrderByDescending is necessary for correct flag parsing when dealing with composite enum values. This ensures the output string uses the most specific (composite) flag names when possible. Removing the ordering would break this logic for flags enums with composite members.
Reasoning
-
Flags enums can have members that represent combinations of other members (composite flags). For example:
- Read = 1
- Write = 2
- ReadWrite = 3 (which is Read | Write)
-
If you process members in ascending order, you may match and consume the individual flags (Read, Write) before considering the composite flag (ReadWrite). This would prevent the composite flag from ever being matched.
-
By sorting in descending order, composite flags (with higher values) are considered first. This ensures that the largest possible flag combinations are matched before their individual components.
Example
Suppose value = 3 and members are:
- Read = 1
- Write = 2
- ReadWrite = 3
If you process in ascending order:
- Match Read (1), remaining = 2
- Match Write (2), remaining = 0
- ReadWrite (3) is never matched
If you process in descending order:
• Match ReadWrite (3), remaining = 0
• Done
There was a problem hiding this comment.
Can we make the 'Members' orderred always then we don't need to do order again and again?
There was a problem hiding this comment.
@xuzhg This is possible. I have made the change here https://github.com/OData/odata.net/pull/3324/files#diff-bf7c2bf531a2d14a23a047276ee7fd40e4b2d6e5b0a1a2a550ba9c531163081d
|
/AzurePipelines run |
2 similar comments
|
/AzurePipelines run |
|
/AzurePipelines run |
| Assert.Equal((AccessLevel.Write | AccessLevel.Execute | AccessLevel.Read), enumResult[0]); | ||
| Assert.Equal((AccessLevel.ReadWrite | AccessLevel.Execute), enumResult[0]); |
There was a problem hiding this comment.
I'm not sure I understand why 2 assert statements
There was a problem hiding this comment.
| var result = new List<string>(); | ||
| long remaining = value; | ||
|
|
||
| for (int index = enumType.Members.Count() - 1; index >= 0; index--) |
There was a problem hiding this comment.
Calling Members.Count() and Members.ElementAt(index) in a loop can turn this into an O(n^2) operation.
I'd suggest you first call .ToList():
List<IEdmEnumMember> members = enumType.Members.ToList();There was a problem hiding this comment.
After doing the above, you can also avoid the string.Reverse call you're doing at the end by iterating the elements in the order you intend to have them in.
There was a problem hiding this comment.
Suppose value = 3 and members are:
- Read = 1
- Write = 2
- ReadWrite = 3
If you process in ascending order:
- Match Read (1), remaining = 2
- Match Write (2), remaining = 0
- ReadWrite (3) is never matched
If you process in descending order:
- Match ReadWrite (3), remaining = 0
- Done
In general, iterating in declaration order would allow us to avoid the Reverse call. However, for flag enums that include composite members (e.g., ReadWrite = 3), processing in ascending order would cause the individual flags (Read = 1, Write = 2) to consume the bits before the composite member is checked, so the composite would never match.
By iterating in descending order (from highest to lowest value), we ensure that composite members are matched first, which is necessary for correct flag decomposition. The Reverse at the end is then required to present the result in declaration order. This approach ensures correct handling for all flag enum scenarios, including those with composite members.
|
/AzurePipelines run |
|
/AzurePipelines run |
Co-authored-by: John Gathogo <john.gathogo@microsoft.com>
Co-authored-by: John Gathogo <john.gathogo@microsoft.com>
|
/AzurePipelines run |
|
/AzurePipelines run |
| // Iterate members in reverse order to match higher-value flags first. | ||
| for (int index = members.Count - 1; index >= 0; index--) | ||
| { | ||
| ulong flagValue = Convert.ToUInt64(members[index].Value.Value); |
There was a problem hiding this comment.
I don't think the logic here is right. I think it should be:
| ulong flagValue = Convert.ToUInt64(members[index].Value.Value); | |
| ulong flagValue = unchecked((ulong)Convert.ToInt64(members[index].Value.Value)); |
Illustration:
string negativeIntAsString = "-1";
Convert.ToUInt64(negativeIntAsString); // throws overflow exception
unchecked((ulong)Convert.ToInt64(negativeIntAsString)) // 18446744073709551615There was a problem hiding this comment.
I'd actually suggest putting the entire block in an unchecked block because of the other operations involving the ulong values
unchecked
{
long remaining = (ulong)value;
// Iterate members in reverse order to match higher-value flags first.
for (int index = members.Count - 1; index >= 0; index--)
{
ulong flagValue = (ulong)Convert.ToInt64(members[index].Value.Value);
if (flagValue != 0 && (remaining & flagValue) == flagValue)
{
result.Add(members[index].Name);
remaining &= ~flagValue; // Remove matched bits
}
}
}There was a problem hiding this comment.
The method ParseFlagsFromIntegralValue uses long instead of ulong for its value parameter and internal calculations. This is intentional because the IEdmEnumMemberValue.Value property is defined as long, ensuring type consistency throughout the method.
The core logic works as follows:
- The input value (of type long) is assigned to a variable called remaining.
- The method iterates through all enum members, checking if each member’s value is present in the remaining bits of the input.
- For each matching flag, the corresponding member name is added to the result, and its value is subtracted from remaining using bitwise operations.
- This process continues until all set flags are accounted for, or until no more matches are possible.
By using long, the method avoids unnecessary type conversions and aligns with the EDM enum member value type, ensuring correctness when reducing the flag values.
|
/AzurePipelines run |
* Add support for Flags enum * Resolve for composite flags and enableCaseInsensitive * Normalize Enum Collection Items --------- Co-authored-by: John Gathogo <john.gathogo@microsoft.com>
* Add support for Flags enum * Resolve for composite flags and enableCaseInsensitive * Normalize Enum Collection Items --------- Co-authored-by: John Gathogo <john.gathogo@microsoft.com>
Issues
This pull request fixes #3244, fixes #3226, fixes #3178
Description
OData queries fail when filtering against Flags enum properties using combined enum values, whether specified as string representations (
'Premium,Loyal') or numeric equivalents (66).The current implementation in
MetadataBindingUtils.csdoes not properly handle:Changes Made
Query Patterns Now Supported
OData ABNF rules for enums
https://docs.oasis-open.org/odata/odata/v4.01/os/abnf/odata-abnf-construction-rules.txt

Rule 1: Enum literals follow the format
qualifiedEnumTypeName'SQUOTE enumValue SQUOTE.qualifiedEnumTypeName(e.g.,Namespace.Color) is optional.Namespace.Color'Blue'or'Blue'.Rule 2:
enumValuecan be a single value or multiple comma-separated values (for flags).'Blue','Red,Green'.Rule 3: Each
singleEnumValuecan be a named member (Red) or a numeric value (4).Rule 4: Numeric values must be valid
int64.'8'or8.Checklist (Uncheck if it is not completed)
Additional work necessary
If documentation update is needed, please add "Docs Needed" label to the issue and provide details about the required document change in the issue.
Repository notes
Team members can start a CI build by adding a comment with the text
/AzurePipelines runto a PR. A bot may respond indicating that there is no pipeline associated with the pull request. This can be ignored if the build is triggered.Team members should not trigger a build this way for pull requests coming from forked repositories. They should instead trigger the build manually by setting the "branch" to
refs/pull/{prId}/mergewhere{prId}is the ID of the PR.