Skip to content

Fix parsing issue for Flags enum values with combined flags#3324

Merged
WanjohiSammy merged 89 commits intomainfrom
fix/3244-support-flags-enum
Nov 12, 2025
Merged

Fix parsing issue for Flags enum values with combined flags#3324
WanjohiSammy merged 89 commits intomainfrom
fix/3244-support-flags-enum

Conversation

@WanjohiSammy
Copy link
Copy Markdown
Member

@WanjohiSammy WanjohiSammy commented Aug 5, 2025

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.cs does not properly handle:

  • Combined flag values in string format (e.g., 'Premium,Loyal')
  • Numeric representations of combined flags (e.g., 66 for Premium | Loyal)

Changes Made

  • Enhanced enum value parsing logic in MetadataBindingUtils.cs
  • Added support for comma-separated flag combinations in string format
  • Implemented numeric value parsing for combined flags
  • Updated validation logic to recognize valid flag combinations

Query Patterns Now Supported

  • /customers?filter=Type eq 'Premium,Loyal'
  • /customers?filter=Type eq 66
  • /customers?filter=Type in ('Premium', 'VIP,Regular,Returning')
  • /customers?filter=Type in (12, 44)
  • /customers?filter=Type in ('12', '44')

OData ABNF rules for enums

https://docs.oasis-open.org/odata/odata/v4.01/os/abnf/odata-abnf-construction-rules.txt
image

  • Rule 1: Enum literals follow the format qualifiedEnumTypeName'SQUOTE enumValue SQUOTE.

    • qualifiedEnumTypeName (e.g., Namespace.Color) is optional.
    • Example: Namespace.Color'Blue' or 'Blue'.
  • Rule 2: enumValue can be a single value or multiple comma-separated values (for flags).

    • Examples: 'Blue', 'Red,Green'.
  • Rule 3: Each singleEnumValue can be a named member (Red) or a numeric value (4).

    • Supports unknown or bitwise values.
  • Rule 4: Numeric values must be valid int64.

    • Example: '8' or 8.

Checklist (Uncheck if it is not completed)

  • Test cases added
  • Build and test with one-click build and test script passed

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 run to 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}/merge where {prId} is the ID of the PR.

@WanjohiSammy
Copy link
Copy Markdown
Member Author

/AzurePipelines run

@azure-pipelines
Copy link
Copy Markdown

No pipelines are associated with this pull request.

@WanjohiSammy
Copy link
Copy Markdown
Member Author

/AzurePipelines run

@azure-pipelines
Copy link
Copy Markdown

No pipelines are associated with this pull request.

@WanjohiSammy WanjohiSammy marked this pull request as ready for review August 7, 2025 09:55
@WanjohiSammy WanjohiSammy requested review from ElizabethOkerio, gathogojr, habbes and xuzhg and removed request for xuzhg August 7, 2025 09:55
@WanjohiSammy WanjohiSammy force-pushed the fix/3244-support-flags-enum branch from 8e03a95 to 2bfab4c Compare August 23, 2025 17:23
@WanjohiSammy
Copy link
Copy Markdown
Member Author

/AzurePipelines run

@azure-pipelines
Copy link
Copy Markdown

No pipelines are associated with this pull request.

@WanjohiSammy
Copy link
Copy Markdown
Member Author

/AzurePipelines run

{
// Sort members by descending flag value to prioritize composite flags
var members = enumType.Members
.OrderByDescending(m => m.Value.Value);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OrderBy is necessary? I cannot see any advantage to use the orderred members?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make the 'Members' orderred always then we don't need to do order again and again?

Copy link
Copy Markdown
Member Author

@WanjohiSammy WanjohiSammy Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

xuzhg
xuzhg previously approved these changes Oct 22, 2025
@WanjohiSammy
Copy link
Copy Markdown
Member Author

/AzurePipelines run

2 similar comments
@WanjohiSammy
Copy link
Copy Markdown
Member Author

/AzurePipelines run

@WanjohiSammy
Copy link
Copy Markdown
Member Author

/AzurePipelines run

Comment on lines +107 to +108
Assert.Equal((AccessLevel.Write | AccessLevel.Execute | AccessLevel.Read), enumResult[0]);
Assert.Equal((AccessLevel.ReadWrite | AccessLevel.Execute), enumResult[0]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand why 2 assert statements

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

var result = new List<string>();
long remaining = value;

for (int index = enumType.Members.Count() - 1; index >= 0; index--)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@WanjohiSammy
Copy link
Copy Markdown
Member Author

/AzurePipelines run

@WanjohiSammy
Copy link
Copy Markdown
Member Author

/AzurePipelines run

WanjohiSammy and others added 2 commits November 11, 2025 18:47
Co-authored-by: John Gathogo <john.gathogo@microsoft.com>
Co-authored-by: John Gathogo <john.gathogo@microsoft.com>
@WanjohiSammy
Copy link
Copy Markdown
Member Author

/AzurePipelines run

@WanjohiSammy
Copy link
Copy Markdown
Member Author

/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);
Copy link
Copy Markdown
Contributor

@gathogojr gathogojr Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the logic here is right. I think it should be:

Suggested change
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)) // 18446744073709551615

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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     
        }
    }
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@WanjohiSammy
Copy link
Copy Markdown
Member Author

/AzurePipelines run

@WanjohiSammy WanjohiSammy merged commit 74b1e45 into main Nov 12, 2025
2 checks passed
@WanjohiSammy WanjohiSammy deleted the fix/3244-support-flags-enum branch November 12, 2025 07:12
WanjohiSammy added a commit that referenced this pull request Nov 13, 2025
* Add support for Flags enum
* Resolve for composite flags and enableCaseInsensitive
* Normalize Enum Collection Items

---------

Co-authored-by: John Gathogo <john.gathogo@microsoft.com>
WanjohiSammy added a commit that referenced this pull request Nov 20, 2025
* Add support for Flags enum
* Resolve for composite flags and enableCaseInsensitive
* Normalize Enum Collection Items

---------

Co-authored-by: John Gathogo <john.gathogo@microsoft.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants