Skip to content
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
423924c
Add support for Flags enum
WanjohiSammy May 13, 2025
dae4b68
Resolve for composite flags and enableCaseInsensitive
WanjohiSammy May 21, 2025
2b83a3a
Undo client changes
WanjohiSammy May 21, 2025
cbc1745
Remove unused usings
WanjohiSammy May 21, 2025
58a8120
Normalize Enum Collection Items
WanjohiSammy May 21, 2025
67dd391
Handle enum for generic Contains method
WanjohiSammy May 26, 2025
8770f96
refactor
WanjohiSammy Jun 4, 2025
32067ec
nit
WanjohiSammy Aug 5, 2025
e6f6da7
Check if the value is integral first
WanjohiSammy Aug 5, 2025
320caac
Add E2E tests
WanjohiSammy Aug 5, 2025
15406d1
resolve failed E2E tests
WanjohiSammy Aug 7, 2025
63e7772
refactor
WanjohiSammy Aug 25, 2025
10e1ad8
Add support for Flags enum
WanjohiSammy May 13, 2025
d0ad6ad
Undo client changes
WanjohiSammy May 21, 2025
37dea34
Remove unused usings
WanjohiSammy May 21, 2025
cb22ea8
Handle enum for generic Contains method
WanjohiSammy May 26, 2025
8d4c6c5
Check if the value is integral first
WanjohiSammy Aug 5, 2025
8840be9
nit
WanjohiSammy Sep 12, 2025
34328c1
nit
WanjohiSammy Oct 28, 2025
27d7b12
Use StringComparison.OrdinalIgnoreCase in case of enableCaseInsensiti…
WanjohiSammy Oct 28, 2025
0a8d79e
nit
WanjohiSammy Oct 28, 2025
65eb52a
making EnumType.Members always ordered
WanjohiSammy Nov 7, 2025
d3ed770
nit
WanjohiSammy Nov 7, 2025
29a92e1
Add support for Flags enum
WanjohiSammy May 13, 2025
bd2b914
Resolve for composite flags and enableCaseInsensitive
WanjohiSammy May 21, 2025
1b9c55d
Undo client changes
WanjohiSammy May 21, 2025
06b5b0d
Remove unused usings
WanjohiSammy May 21, 2025
d2f6940
Normalize Enum Collection Items
WanjohiSammy May 21, 2025
8f60c6c
Handle enum for generic Contains method
WanjohiSammy May 26, 2025
1de41d7
refactor
WanjohiSammy Jun 4, 2025
12c4a37
nit
WanjohiSammy Aug 5, 2025
f7670d0
Check if the value is integral first
WanjohiSammy Aug 5, 2025
2c8cb0b
Add E2E tests
WanjohiSammy Aug 5, 2025
4e99c72
resolve failed E2E tests
WanjohiSammy Aug 7, 2025
29003b8
refactor
WanjohiSammy Aug 25, 2025
c7987af
Add support for Flags enum
WanjohiSammy May 13, 2025
e642006
Undo client changes
WanjohiSammy May 21, 2025
7bb192b
Remove unused usings
WanjohiSammy May 21, 2025
69a5751
Handle enum for generic Contains method
WanjohiSammy May 26, 2025
186dd74
Check if the value is integral first
WanjohiSammy Aug 5, 2025
c9367e9
nit
WanjohiSammy Sep 12, 2025
e70ba44
nit
WanjohiSammy Oct 28, 2025
c77fdc9
Use StringComparison.OrdinalIgnoreCase in case of enableCaseInsensiti…
WanjohiSammy Oct 28, 2025
e9ac11d
nit
WanjohiSammy Oct 28, 2025
cbeb977
making EnumType.Members always ordered
WanjohiSammy Nov 7, 2025
5c366d4
nit
WanjohiSammy Nov 7, 2025
af55733
Merge branch 'fix/3244-support-flags-enum' of https://github.com/ODat…
WanjohiSammy Nov 7, 2025
4ebed58
Add support for Flags enum
WanjohiSammy May 13, 2025
b9fd1c4
Resolve for composite flags and enableCaseInsensitive
WanjohiSammy May 21, 2025
4d62939
Undo client changes
WanjohiSammy May 21, 2025
d37e00b
Remove unused usings
WanjohiSammy May 21, 2025
fc73df2
Normalize Enum Collection Items
WanjohiSammy May 21, 2025
25fd8fa
Handle enum for generic Contains method
WanjohiSammy May 26, 2025
1a685ab
refactor
WanjohiSammy Jun 4, 2025
515fead
nit
WanjohiSammy Aug 5, 2025
32f6795
Check if the value is integral first
WanjohiSammy Aug 5, 2025
6bde046
Add E2E tests
WanjohiSammy Aug 5, 2025
c0afc7f
resolve failed E2E tests
WanjohiSammy Aug 7, 2025
f932250
refactor
WanjohiSammy Aug 25, 2025
07982fa
Add support for Flags enum
WanjohiSammy May 13, 2025
96a9435
Undo client changes
WanjohiSammy May 21, 2025
d21c367
Remove unused usings
WanjohiSammy May 21, 2025
a5732ab
Handle enum for generic Contains method
WanjohiSammy May 26, 2025
ed149b4
Check if the value is integral first
WanjohiSammy Aug 5, 2025
33896d5
nit
WanjohiSammy Sep 12, 2025
8bf49e3
nit
WanjohiSammy Oct 28, 2025
8e958e2
Use StringComparison.OrdinalIgnoreCase in case of enableCaseInsensiti…
WanjohiSammy Oct 28, 2025
6c69bfa
nit
WanjohiSammy Oct 28, 2025
a7f9292
making EnumType.Members always ordered
WanjohiSammy Nov 7, 2025
d6309f8
nit
WanjohiSammy Nov 7, 2025
41ad1ac
Add support for Flags enum
WanjohiSammy May 13, 2025
00e849e
Undo client changes
WanjohiSammy May 21, 2025
2d9877a
Remove unused usings
WanjohiSammy May 21, 2025
56de19b
Handle enum for generic Contains method
WanjohiSammy May 26, 2025
f99d034
Check if the value is integral first
WanjohiSammy Aug 5, 2025
aff7557
Add support for Flags enum
WanjohiSammy May 13, 2025
fbf4ac5
Undo client changes
WanjohiSammy May 21, 2025
6e1f709
Remove unused usings
WanjohiSammy May 21, 2025
41baf73
Handle enum for generic Contains method
WanjohiSammy May 26, 2025
9384bc6
Check if the value is integral first
WanjohiSammy Aug 5, 2025
831ce7b
Merge branch 'fix/3244-support-flags-enum' of https://github.com/ODat…
WanjohiSammy Nov 11, 2025
171e219
add tests that throws
WanjohiSammy Nov 11, 2025
01d083f
handle 0 if present
WanjohiSammy Nov 11, 2025
4e69342
Add more tests and ensure valid memberName should not start or ends w…
WanjohiSammy Nov 11, 2025
84e269b
Add more E2E tests
WanjohiSammy Nov 11, 2025
7e44588
Update src/Microsoft.OData.Core/EdmExtensionMethods.cs
WanjohiSammy Nov 11, 2025
bc0eb18
Update src/Microsoft.OData.Core/EdmExtensionMethods.cs
WanjohiSammy Nov 11, 2025
4fdbd79
removing space after `,`
WanjohiSammy Nov 11, 2025
43293b9
use long instead of ulong
WanjohiSammy Nov 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 137 additions & 2 deletions src/Microsoft.OData.Core/EdmExtensionMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using Microsoft.OData.Edm;
using Microsoft.OData.UriParser;

Expand Down Expand Up @@ -152,17 +154,150 @@ public static bool TryParse(this IEdmEnumType enumType, long value, out IEdmEnum
/// <param name="memberName">The member name to check.</param>
/// <param name="comparison">The comparison type to use for string comparison. Default is Ordinal.</param>
/// <returns>True if the member name exists in the enum type; otherwise, false.</returns>
public static bool ContainsMember(this IEdmEnumType enumType, string memberName, StringComparison comparison = StringComparison.Ordinal)
public static bool HasMember(this IEdmEnumType enumType, string memberName, StringComparison comparison = StringComparison.Ordinal)
{
return enumType.HasMember(memberName.AsSpan(), comparison);
}

/// <summary>
/// Determines whether the specified enum type contains a member with the given name, using the specified string comparison.
/// </summary>
/// <param name="enumType">The <see cref="IEdmEnumType"/> to search for the member.</param>
/// <param name="memberName">The name of the member to locate within the enum type.</param>
/// <param name="exactMemberName">When this method returns, contains the <see cref="IEdmEnumMember"/> that matches the specified name, if a match is found; otherwise, null.</param>
/// <param name="comparison">The comparison type to use for string comparison. Default is Ordinal.</param>
/// <returns>True if the member name exists in the enum type; otherwise, false.</returns>
public static IEdmEnumMember FindMember(this IEdmEnumType enumType, string memberName, StringComparison comparison = StringComparison.Ordinal)
{
return enumType.FindMember(memberName.AsSpan(), comparison);
}

/// <summary>
/// Checks if the given member name exists in the enum type.
/// </summary>
/// <param name="enumType">The enum type to search for the member.</param>
/// <param name="memberName">The name of the member to locate, represented as a <see cref="ReadOnlySpan{T}"/> of characters.</param>
/// <param name="comparison">The <see cref="StringComparison"/> to use when comparing the member names. The default is <see cref="StringComparison.Ordinal"/>.</param>
/// <returns>True if the member name exists in the enum type; otherwise, false.</returns>
public static bool HasMember(this IEdmEnumType enumType, ReadOnlySpan<char> memberName, StringComparison comparison = StringComparison.Ordinal)
{
foreach (IEdmEnumMember member in enumType.Members)
{
if (string.Equals(member.Name, memberName, comparison))
if (memberName.Equals(member.Name.AsSpan(), comparison))
{
return true;
}
}

return false;
}

/// <summary>
/// Checks if the given member name exists in the enum type.
/// </summary>
/// <param name="enumType">The enum type to search for the member.</param>
/// <param name="memberName">The name of the member to locate, represented as a <see cref="ReadOnlySpan{T}"/> of characters.</param>
/// <param name="exactMemberName">When this method returns, contains the <see cref="IEdmEnumMember"/> that matches the specified name, if a match is found; otherwise, null.</param>
/// <param name="comparison">The <see cref="StringComparison"/> to use when comparing the member names. The default is <see cref="StringComparison.Ordinal"/>.</param>
/// <returns>True if the member name exists in the enum type; otherwise, false.</returns>
public static IEdmEnumMember FindMember(this IEdmEnumType enumType, ReadOnlySpan<char> memberName, StringComparison comparison = StringComparison.Ordinal)
{
foreach (IEdmEnumMember member in enumType.Members)
{
if (memberName.Equals(member.Name.AsSpan(), comparison))
{
return member;
}
}

return null;
}

/// <summary>
/// Parses the specified integral value into a comma-separated string of flag names based on the members of the given EDM enum type.
/// </summary>
/// <param name="enumType">The EDM enum type containing the members to parse.</param>
/// <param name="value">The integral value to parse into flag names.</param>
/// <returns>A comma-separated string of flag names corresponding to the set bits in the specified value. Returns null otherwise.</returns>
public static string ParseFlagsFromIntegralValue(this IEdmEnumType enumType, long value)
{
var result = new List<string>();
long remaining = value;

for (int index = enumType.Members.Count() - 1; index >= 0; index--)
Copy link
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
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
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.

{
IEdmEnumMember member = enumType.Members.ElementAt(index);
long flagValue = Convert.ToInt64(member.Value.Value);
if (flagValue != 0 && (remaining & flagValue) == flagValue)
{
result.Add(member.Name);
remaining &= ~flagValue; // Remove matched bits
}
}

return result.Count > 0 && remaining == 0 ? string.Join(", ", result.Reverse<string>()) : null;
}

/// <summary>
/// Parses a comma-separated string of enum member names into a validated, formatted string containing only valid members of the specified <see cref="IEdmEnumType"/>.
/// </summary>
/// <param name="enumType">The EDM enum type to validate the enum member names against.</param>
/// <param name="memberName">A comma-separated string containing the names of enum members to parse and validate.</param>
/// <param name="comparison">The <see cref="StringComparison"/> to use when comparing the provided member names against the enum type's
/// defined members.</param>
/// <returns>A formatted string containing the validated enum member names, separated by commas and trimmed of whitespace. Otherwise, null or empty string.</returns>
public static string ParseFlagsFromStringValue(this IEdmEnumType enumType, string memberName, StringComparison comparison)
{
var stringBuilder = new StringBuilder();
int startIndex = 0, endIndex = 0;
while (endIndex < memberName.Length)
{
while (endIndex < memberName.Length && memberName[endIndex] != ',')
{
endIndex++;
}

ReadOnlySpan<char> currentValue = memberName.AsSpan()[startIndex..endIndex].Trim();
IEdmEnumMember edmEnumMember = enumType.FindMember(currentValue, comparison);
if (edmEnumMember == null)
{
return null;
}

if (stringBuilder.Length > 0)
{
stringBuilder.Append(", ");
}

stringBuilder.Append(edmEnumMember.Name);
startIndex = endIndex + 1;
endIndex = startIndex;
}

return stringBuilder.Length == 0 ? null : stringBuilder.ToString();
}

/// <summary>
/// Determines whether the specified integral value is a valid combination of flags for the given enumeration type.
/// </summary>
/// <param name="enumType">The enumeration type to validate against. Must represent a flags enumeration.</param>
/// <param name="memberIntegralValue">The integral value to validate as a combination of flags.</param>
/// <returns><see langword="true"/> if the specified value is a valid combination of the flags defined in the
/// enumeration; otherwise, <see langword="false"/>.</returns>
public static bool IsValidFlagsEnumValue(this IEdmEnumType enumType, long memberIntegralValue)
{
if(enumType == null || !enumType.IsFlags)
{
return false;
}

long allFlagsMask = 0;
foreach (IEdmEnumMember member in enumType.Members)
{
allFlagsMask |= (long)member.Value.Value;
}

return (memberIntegralValue & ~allFlagsMask) == 0;
}
}
}
108 changes: 107 additions & 1 deletion src/Microsoft.OData.Core/UriParser/Binders/InBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ internal QueryNode BindInOperator(InToken inToken, BindingState state)
// Calls the MetadataBindingUtils.ConvertToTypeIfNeeded() method to convert the left operand to the same enum type as the right operand.
if ((!(right is CollectionConstantNode) && right.ItemType.IsEnum()) && (left.TypeReference != null && (left.TypeReference.IsString() || left.TypeReference.IsIntegral())))
{
left = MetadataBindingUtils.ConvertToTypeIfNeeded(left, right.ItemType);
left = MetadataBindingUtils.ConvertToTypeIfNeeded(left, right.ItemType, this.resolver.EnableCaseInsensitive);
}

MetadataBindingUtils.VerifyCollectionNode(right, this.resolver.EnableCaseInsensitive);
Expand Down Expand Up @@ -157,6 +157,10 @@ private CollectionNode GetCollectionOperandFromToken(QueryToken queryToken, IEdm
// ==> ['1970-01-01T00:00:00Z', '1980-01-01T01:01:01+01:00']
bracketLiteralText = NormalizeDateTimeCollectionItems(bracketLiteralText);
}
else if (expectedType.Definition.AsElementType().TypeKind == EdmTypeKind.Enum)
{
bracketLiteralText = NormalizeEnumCollectionItems(bracketLiteralText, this.resolver.EnableCaseInsensitive, expectedTypeFullName);
}
}

object collection = ODataUriConversionUtils.ConvertFromCollectionValue(bracketLiteralText, model, expectedType);
Expand Down Expand Up @@ -426,6 +430,108 @@ private static string NormalizeDateTimeCollectionItems(string bracketLiteralText
return "[" + String.Join(",", items) + "]";
}

private static string NormalizeEnumCollectionItems(string bracketLiteralText, bool enableCaseInsensitive, string expectedTypeFullName)
{
// Remove the '[' and ']' or '(' and ')' and trim the content
ReadOnlySpan<char> normalizedText = bracketLiteralText.AsSpan(1, bracketLiteralText.Length - 2);

// Trim leading/trailing whitespace
int left = 0;
int right = normalizedText.Length - 1;
while (left <= right && char.IsWhiteSpace(normalizedText[left]))
{
left++;
}

while (right >= left && char.IsWhiteSpace(normalizedText[right]))
{
right--;
}

if (left > right)
{
return "[]";
}

int expectedTypeFullNameLength = string.IsNullOrEmpty(expectedTypeFullName) ? 0 : expectedTypeFullName.Length;
ReadOnlySpan<char> content = normalizedText.Slice(left, right - left + 1);
int startIndex = 0;
int length = content.Length;

StringBuilder result = new StringBuilder(length + 2);
result.Append('[');

while (startIndex < length)
{
char currentChar = content[startIndex];

if (currentChar == '\'' || currentChar == '"')
{
// Handle quoted items
int relativeEnd = content.Slice(startIndex + 1).IndexOf(currentChar);
if (relativeEnd < 0)
{
throw new ODataException(Error.Format("Found unbalanced quotes '{0}'", currentChar));
}

// Include closing quote
int endIndex = startIndex + 1 + relativeEnd;
result.Append(content.Slice(startIndex, endIndex - startIndex + 1));
startIndex = endIndex + 1;
}
else if (currentChar == ',')
{
// Handle commas
result.Append(',');
startIndex++;
}
else if (char.IsWhiteSpace(currentChar))
{
// Skip whitespace
startIndex++;
}
else
{
// Handle non-quoted items
int end = startIndex;
while (end < length && content[end] != ',' && !char.IsWhiteSpace(content[end]))
{
end++;
}

ReadOnlySpan<char> token = content[startIndex..end];

// Remove type prefix if present e.g., Namespace.Days'Monday'
if (expectedTypeFullNameLength > 0 && token.Length > expectedTypeFullNameLength &&
token.StartsWith(expectedTypeFullName, enableCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))
{
char next = token[expectedTypeFullNameLength];
if (next == '\'' || next == '\"')
{
token = token.Slice(expectedTypeFullNameLength);
}
}

// If item is already quoted, keep it; otherwise wrap in single quotes
if (token.Length > 0 && (token[0] == '\'' || token[0] == '"'))
{
result.Append(token);
}
else
{
result.Append('\'');
result.Append(token);
result.Append('\'');
}

startIndex = end;
}
}

result.Append(']');
return result.ToString();
}

private static bool IsCollectionEmptyOrWhiteSpace(string bracketLiteralText)
{
string content = bracketLiteralText[1..^1].Trim();
Expand Down
Loading