Skip to content
Open
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
dd30c17
Add support for Flags enum
WanjohiSammy May 13, 2025
739d2a3
Resolve for composite flags and enableCaseInsensitive
WanjohiSammy May 21, 2025
a136ef1
Merge remote-tracking branch 'origin' into fix/3244-support-flags-enum
WanjohiSammy May 21, 2025
86790b7
Undo client changes
WanjohiSammy May 21, 2025
3ad2641
Remove unused usings
WanjohiSammy May 21, 2025
eb67194
Normalize Enum Collection Items
WanjohiSammy May 21, 2025
33fd670
Handle enum for generic Contains method
WanjohiSammy May 26, 2025
1980f50
refactor
WanjohiSammy Jun 4, 2025
f65efcd
Merge remote-tracking branch 'origin' into fix/3244-support-flags-enum
WanjohiSammy Aug 5, 2025
33b88f3
nit
WanjohiSammy Aug 5, 2025
943ab31
Check if the value is integral first
WanjohiSammy Aug 5, 2025
bab3a50
Add E2E tests
WanjohiSammy Aug 5, 2025
8e03a95
resolve failed E2E tests
WanjohiSammy Aug 7, 2025
85e9269
Add support for Flags enum
WanjohiSammy May 13, 2025
c2fecf4
Resolve for composite flags and enableCaseInsensitive
WanjohiSammy May 21, 2025
03bfe22
Undo client changes
WanjohiSammy May 21, 2025
fc867e3
Remove unused usings
WanjohiSammy May 21, 2025
f6dcd4b
Normalize Enum Collection Items
WanjohiSammy May 21, 2025
f0b767c
Handle enum for generic Contains method
WanjohiSammy May 26, 2025
c61e92c
refactor
WanjohiSammy Jun 4, 2025
0832cf8
nit
WanjohiSammy Aug 5, 2025
65d452b
Check if the value is integral first
WanjohiSammy Aug 5, 2025
dc163ff
Add E2E tests
WanjohiSammy Aug 5, 2025
2bfab4c
resolve failed E2E tests
WanjohiSammy Aug 7, 2025
46c4187
refactor
WanjohiSammy Aug 25, 2025
3e62624
resolve conflicts
WanjohiSammy Aug 25, 2025
a69deea
nit
WanjohiSammy Sep 12, 2025
1f3bffa
Merge remote-tracking branch 'origin' into fix/3244-support-flags-enum
WanjohiSammy Oct 8, 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
157 changes: 157 additions & 0 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 @@ -164,5 +166,160 @@ public static bool ContainsMember(this IEdmEnumType enumType, string memberName,

return false;
}

/// <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 bool ContainsMember(this IEdmEnumType enumType, string memberName, out IEdmEnumMember exactMemberName, StringComparison comparison = StringComparison.Ordinal)
{
foreach (IEdmEnumMember member in enumType.Members)
{
if (string.Equals(member.Name, memberName, comparison))
{
exactMemberName = member;
return true;
}
}

exactMemberName = null;
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="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 ContainsMember(this IEdmEnumType enumType, ReadOnlySpan<char> memberName, StringComparison comparison = StringComparison.Ordinal)
{
foreach (IEdmEnumMember member in enumType.Members)
{
if (member.Name.AsSpan().Equals(memberName, 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 bool ContainsMember(this IEdmEnumType enumType, ReadOnlySpan<char> memberName, out IEdmEnumMember exactMemberName, StringComparison comparison = StringComparison.Ordinal)
{
foreach (IEdmEnumMember member in enumType.Members)
{
if (member.Name.AsSpan().Equals(memberName, comparison))
{
exactMemberName = member;
return true;
}
}

exactMemberName = null;
return false;
}

/// <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)
{
// Sort members by descending flag value to prioritize composite flags
var members = enumType.Members
.OrderByDescending(m => m.Value.Value);

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

foreach (IEdmEnumMember member in members)
{
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 start = 0, end = 0;
while (end < memberName.Length)
{
while (end < memberName.Length && memberName[end] != ',')
{
end++;
}

ReadOnlySpan<char> currentValue = memberName.AsSpan()[start..end].Trim();
if (!enumType.ContainsMember(currentValue, out IEdmEnumMember edmEnumMember, comparison))
{
return null;
}

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

stringBuilder.Append(edmEnumMember.Name);
start = end + 1;
end = start;
}

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

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

return (memberIntegralValue & ~allFlagsMask) == 0;
}
}
}
111 changes: 110 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, expectedTypeFullName);
}
}

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

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

// 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 expectedTypeFullNameLen = string.IsNullOrEmpty(expectedTypeFullName) ? 0 : expectedTypeFullName.Length;
ReadOnlySpan<char> content = normalizedText.Slice(left, right - left + 1);
int start = 0;
int length = content.Length;

StringBuilder result = new StringBuilder();
result.Append('[');

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

if (currentChar == '\'' || currentChar == '"')
{
// Handle quoted items
char quoteChar = currentChar;
result.Append(quoteChar);
int end = ++start;

while (end < length && content[end] != quoteChar)
{
result.Append(content[end]);
end++;
}

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

ReadOnlySpan<char> token = content[start..end];
if (expectedTypeFullNameLen > 0 && token.Length > expectedTypeFullNameLen &&
token.StartsWith(expectedTypeFullName, StringComparison.Ordinal))
{
char next = token[expectedTypeFullNameLen];
if (next == '\'' || next == '\"')
{
token = token.Slice(expectedTypeFullNameLen);
}
}

// 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('\'');
}

start = end;
}
}

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

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