diff --git a/src/Microsoft.OData.Core/EdmExtensionMethods.cs b/src/Microsoft.OData.Core/EdmExtensionMethods.cs index 9a99491efb..a34da9b89d 100644 --- a/src/Microsoft.OData.Core/EdmExtensionMethods.cs +++ b/src/Microsoft.OData.Core/EdmExtensionMethods.cs @@ -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; @@ -164,5 +166,160 @@ public static bool ContainsMember(this IEdmEnumType enumType, string memberName, return false; } + + /// + /// Determines whether the specified enum type contains a member with the given name, using the specified string comparison. + /// + /// The to search for the member. + /// The name of the member to locate within the enum type. + /// When this method returns, contains the that matches the specified name, if a match is found; otherwise, null. + /// The comparison type to use for string comparison. Default is Ordinal. + /// True if the member name exists in the enum type; otherwise, false. + 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; + } + + /// + /// Checks if the given member name exists in the enum type. + /// + /// The enum type to search for the member. + /// The name of the member to locate, represented as a of characters. + /// The to use when comparing the member names. The default is . + /// True if the member name exists in the enum type; otherwise, false. + public static bool ContainsMember(this IEdmEnumType enumType, ReadOnlySpan memberName, StringComparison comparison = StringComparison.Ordinal) + { + foreach (IEdmEnumMember member in enumType.Members) + { + if (member.Name.AsSpan().Equals(memberName, comparison)) + { + return true; + } + } + + return false; + } + + /// + /// Checks if the given member name exists in the enum type. + /// + /// The enum type to search for the member. + /// The name of the member to locate, represented as a of characters. + /// When this method returns, contains the that matches the specified name, if a match is found; otherwise, null. + /// The to use when comparing the member names. The default is . + /// True if the member name exists in the enum type; otherwise, false. + public static bool ContainsMember(this IEdmEnumType enumType, ReadOnlySpan 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; + } + + /// + /// Parses the specified integral value into a comma-separated string of flag names based on the members of the given EDM enum type. + /// + /// The EDM enum type containing the members to parse. + /// The integral value to parse into flag names. + /// A comma-separated string of flag names corresponding to the set bits in the specified value. Returns null otherwise. + 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(); + 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()) : null; + } + + /// + /// Parses a comma-separated string of enum member names into a validated, formatted string containing only valid members of the specified . + /// + /// The EDM enum type to validate the enum member names against. + /// A comma-separated string containing the names of enum members to parse and validate. + /// The to use when comparing the provided member names against the enum type's + /// defined members. + /// A formatted string containing the validated enum member names, separated by commas and trimmed of whitespace. Otherwise, null or empty string. + 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 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(); + } + + /// + /// Determines whether the specified integral value is a valid combination of flags for the given enumeration type. + /// + /// The enumeration type to validate against. Must represent a flags enumeration. + /// The integral value to validate as a combination of flags. + /// if the specified value is a valid combination of the flags defined in the + /// enumeration; otherwise, . + 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; + } } } diff --git a/src/Microsoft.OData.Core/UriParser/Binders/InBinder.cs b/src/Microsoft.OData.Core/UriParser/Binders/InBinder.cs index fbf1c8a075..ce84acdb1d 100644 --- a/src/Microsoft.OData.Core/UriParser/Binders/InBinder.cs +++ b/src/Microsoft.OData.Core/UriParser/Binders/InBinder.cs @@ -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); @@ -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); @@ -425,6 +429,106 @@ 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 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 expectedTypeFullNameLen = string.IsNullOrEmpty(expectedTypeFullName) ? 0 : expectedTypeFullName.Length; + ReadOnlySpan 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 token = content[startIndex..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('\''); + } + + startIndex = end; + } + } + + result.Append(']'); + return result.ToString(); + } + private static bool IsCollectionEmptyOrWhiteSpace(string bracketLiteralText) { string content = bracketLiteralText[1..^1].Trim(); diff --git a/src/Microsoft.OData.Core/UriParser/Binders/MetadataBindingUtils.cs b/src/Microsoft.OData.Core/UriParser/Binders/MetadataBindingUtils.cs index d817b415cf..0dfbe7ae40 100644 --- a/src/Microsoft.OData.Core/UriParser/Binders/MetadataBindingUtils.cs +++ b/src/Microsoft.OData.Core/UriParser/Binders/MetadataBindingUtils.cs @@ -8,11 +8,10 @@ namespace Microsoft.OData.UriParser { using System; using System.Diagnostics; - using System.Linq; - using Microsoft.OData.Edm; using Microsoft.OData; - using Microsoft.OData.Metadata; using Microsoft.OData.Core; + using Microsoft.OData.Edm; + using Microsoft.OData.Metadata; /// /// Helper methods for metadata binding. @@ -25,8 +24,9 @@ internal static class MetadataBindingUtils /// /// The source node to apply the conversion to. /// The target primitive type. May be null - this method will do nothing in that case. + /// Whether to enable case insensitive comparison for enum member names. /// The converted query node, or the original source node unchanged. - internal static SingleValueNode ConvertToTypeIfNeeded(SingleValueNode source, IEdmTypeReference targetTypeReference) + internal static SingleValueNode ConvertToTypeIfNeeded(SingleValueNode source, IEdmTypeReference targetTypeReference, bool enableCaseInsensitive = false) { Debug.Assert(source != null, "source != null"); @@ -62,19 +62,49 @@ internal static SingleValueNode ConvertToTypeIfNeeded(SingleValueNode source, IE // and the target type is an enum. if (constantNode != null && constantNode.Value != null && (source.TypeReference.IsString() || source.TypeReference.IsIntegral()) && targetTypeReference.IsEnum()) { + // String comparison for enum member names is case-sensitive by default. + StringComparison comparison = enableCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; + string memberName = constantNode.Value.ToString(); IEdmEnumType enumType = targetTypeReference.Definition as IEdmEnumType; - if (enumType.ContainsMember(memberName, StringComparison.Ordinal)) + + // If the member name is an integral value, we should try to convert it to the enum member name and find the enum member with the matching integral value + if (long.TryParse(memberName, out long memberIntegralValue)) + { + if(enumType.TryParse(memberIntegralValue, out IEdmEnumMember enumMember)) + { + string literalText = ODataUriUtils.ConvertToUriLiteral(enumMember.Name, default(ODataVersion)); + return new ConstantNode(new ODataEnumValue(enumMember.Name, enumType.ToString()), literalText, targetTypeReference); + } + + if(enumType.IsFlags) + { + string flagsValue = enumType.ParseFlagsFromIntegralValue(memberIntegralValue); + if(!string.IsNullOrEmpty(flagsValue)) + { + string literalText = ODataUriUtils.ConvertToUriLiteral(flagsValue, default(ODataVersion)); + return new ConstantNode(new ODataEnumValue(flagsValue, enumType.ToString()), literalText, targetTypeReference); + } + } + } + + // Check if the member name is a valid enum member name + if (enumType.ContainsMember(memberName, out IEdmEnumMember edmEnumMember, comparison)) { string literalText = ODataUriUtils.ConvertToUriLiteral(constantNode.Value, default(ODataVersion)); - return new ConstantNode(new ODataEnumValue(memberName, enumType.ToString()), literalText, targetTypeReference); + return new ConstantNode(new ODataEnumValue(edmEnumMember.Name, enumType.ToString()), literalText, targetTypeReference); } - // If the member name is an integral value, we should try to convert it to the enum member name and find the enum member with the matching integral value - if (long.TryParse(memberName, out long memberIntegralValue) && enumType.TryParse(memberIntegralValue, out IEdmEnumMember enumMember)) + // If the member name is a string representation of a flags value, + // we should try to convert it to the enum member name and find the enum member with the matching flags value + if (enumType.IsFlags) { - string literalText = ODataUriUtils.ConvertToUriLiteral(enumMember.Name, default(ODataVersion)); - return new ConstantNode(new ODataEnumValue(enumMember.Name, enumType.ToString()), literalText, targetTypeReference); + string flagsValue = enumType.ParseFlagsFromStringValue(memberName, comparison); + if (!string.IsNullOrEmpty(flagsValue)) + { + string literalText = ODataUriUtils.ConvertToUriLiteral(flagsValue, default(ODataVersion)); + return new ConstantNode(new ODataEnumValue(flagsValue, enumType.ToString()), literalText, targetTypeReference); + } } throw new ODataException(Error.Format(SRResources.Binder_IsNotValidEnumConstant, memberName)); @@ -195,16 +225,39 @@ internal static void VerifyCollectionNode(CollectionNode node, bool enableCaseIn } IEdmEnumType enumType = collectionConstantNode.ItemType.Definition as IEdmEnumType; - StringComparison comparison = enableCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; + foreach (ConstantNode item in collectionConstantNode.Collection) { if (item != null && item.Value != null && item.Value is ODataEnumValue enumValue) { - if (!enumType.ContainsMember(enumValue.Value, comparison)) + // Check if the enum value is a valid enum member name + if (enumType.ContainsMember(enumValue.Value, comparison)) { - throw new ODataException(Error.Format(SRResources.Binder_IsNotValidEnumConstant, enumValue.Value)); + continue; } + + if (long.TryParse(enumValue.Value, out long memberIntegralValue)) + { + // Check if the enum value is a valid integral value + if (enumType.TryParse(memberIntegralValue, out IEdmEnumMember _)) + { + continue; + } + + // Check if the enum value is a valid flags value + if (enumType.IsFlags && enumType.IsValidFlagsEnumValue(memberIntegralValue)) + { + continue; + } + } + + if (enumType.IsFlags && !string.IsNullOrEmpty(enumType.ParseFlagsFromStringValue(enumValue.Value, comparison))) + { + continue; + } + + throw new ODataException(Error.Format(SRResources.Binder_IsNotValidEnumConstant, enumValue.Value)); } } } diff --git a/test/EndToEndTests/Common/Microsoft.OData.E2E.TestCommon/Common/Server/Asynchronous/AsynchronousDelayQueryTestsController.cs b/test/EndToEndTests/Common/Microsoft.OData.E2E.TestCommon/Common/Server/Asynchronous/AsynchronousDelayQueryTestsController.cs index 4cc5f2b53f..56a6d3277c 100644 --- a/test/EndToEndTests/Common/Microsoft.OData.E2E.TestCommon/Common/Server/Asynchronous/AsynchronousDelayQueryTestsController.cs +++ b/test/EndToEndTests/Common/Microsoft.OData.E2E.TestCommon/Common/Server/Asynchronous/AsynchronousDelayQueryTestsController.cs @@ -119,7 +119,7 @@ public IActionResult GetAllProducts([FromODataUri] int start, [FromODataUri] int [HttpGet("odata/GetProductsByAccessLevel(accessLevel={accessLevel})")] public IActionResult GetProductsByAccessLevel([FromODataUri] AccessLevel accessLevel) { - var count = _dataSource.Products?.Where(p => (p.UserAccess & accessLevel) == accessLevel).Count(); + var count = _dataSource.Products?.Where(p => p.UserAccess == accessLevel).Count(); return Ok((double)count); } diff --git a/test/EndToEndTests/Common/Microsoft.OData.E2E.TestCommon/Common/Server/Default/DefaultDataSource.cs b/test/EndToEndTests/Common/Microsoft.OData.E2E.TestCommon/Common/Server/Default/DefaultDataSource.cs index 7736c8ec79..3e677a903a 100644 --- a/test/EndToEndTests/Common/Microsoft.OData.E2E.TestCommon/Common/Server/Default/DefaultDataSource.cs +++ b/test/EndToEndTests/Common/Microsoft.OData.E2E.TestCommon/Common/Server/Default/DefaultDataSource.cs @@ -327,7 +327,7 @@ public void Initialize() Discontinued = false, SkinColor = Color.Red, CoverColors = [Color.Red, Color.Red, Color.Blue], - UserAccess = AccessLevel.Execute, + UserAccess = AccessLevel.Write | AccessLevel.Execute | AccessLevel.Read, }, new Product() { diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/AsynchronousTests/AsynchronousDelayQueryTests.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/AsynchronousTests/AsynchronousDelayQueryTests.cs index f77c177ae6..c01b33ef65 100644 --- a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/AsynchronousTests/AsynchronousDelayQueryTests.cs +++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/AsynchronousTests/AsynchronousDelayQueryTests.cs @@ -260,6 +260,11 @@ public async Task DelayQuery_OnFunctionImport_Test() var productsCount = await productsQuery.GetValueAsync(); Assert.EndsWith("/GetProductsByAccessLevel(accessLevel=Microsoft.OData.E2E.TestCommon.Common.Server.Default.AccessLevel'Read%2CExecute')", productsQuery.RequestUri.AbsoluteUri); Assert.Equal(1, productsCount); + + productsQuery = context.GetProductsByAccessLevel(AccessLevel.Execute | AccessLevel.Read); + productsCount = await productsQuery.GetValueAsync(); + Assert.EndsWith("/GetProductsByAccessLevel(accessLevel=Microsoft.OData.E2E.TestCommon.Common.Server.Default.AccessLevel'Read%2CExecute')", productsQuery.RequestUri.AbsoluteUri); + Assert.Equal(1, productsCount); } [Fact] diff --git a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/EnumerationTypeTests/EnumerationTypeQueryTests.cs b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/EnumerationTypeTests/EnumerationTypeQueryTests.cs index 8633b84358..0dff65b214 100644 --- a/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/EnumerationTypeTests/EnumerationTypeQueryTests.cs +++ b/test/EndToEndTests/Tests/Client/Microsoft.OData.Client.E2E.Tests/EnumerationTypeTests/EnumerationTypeQueryTests.cs @@ -90,7 +90,8 @@ public void QueryEntityUsingODataClientAndVerifyEnumProperties() var result = queryable.GetValue(); Assert.NotNull(result); Assert.Equal(Color.Red, result.SkinColor); - Assert.Equal(AccessLevel.Execute, result.UserAccess); + Assert.Equal((AccessLevel.Write | AccessLevel.Execute | AccessLevel.Read), result.UserAccess); + Assert.Equal((AccessLevel.ReadWrite | AccessLevel.Execute), result.UserAccess); } [Fact] @@ -103,7 +104,8 @@ public void QueryEnumPropertyUsingODataClientAndVerifyValue() var userAccess = _context.Execute(new Uri(_baseUri.AbsoluteUri + "Products(8)/UserAccess")); List enumResult = userAccess.ToList(); Assert.Single(enumResult); - Assert.Equal(AccessLevel.Execute, enumResult[0]); + Assert.Equal((AccessLevel.Write | AccessLevel.Execute | AccessLevel.Read), enumResult[0]); + Assert.Equal((AccessLevel.ReadWrite | AccessLevel.Execute), enumResult[0]); } [Fact] @@ -135,7 +137,8 @@ public void QueryEntitiesWithQueryOptionsUsingODataClientAndVerifyResults() Assert.True(result.All(s => s.SkinColor == Color.Red)); Assert.NotNull(result.SingleOrDefault(r => r.ProductID == 5 && r.UserAccess == AccessLevel.None)); Assert.NotNull(result.SingleOrDefault(r => r.ProductID == 7 && r.UserAccess == AccessLevel.Read)); - Assert.NotNull(result.SingleOrDefault(r => r.ProductID == 8 && r.UserAccess == AccessLevel.Execute)); + Assert.NotNull(result.SingleOrDefault(r => r.ProductID == 8 && + (r.UserAccess == (AccessLevel.Write | AccessLevel.Execute | AccessLevel.Read) || r.UserAccess == (AccessLevel.ReadWrite | AccessLevel.Execute)))); // FullMetadata: UseJson() + have $select in request uri _context.Format.UseJson(_model); @@ -149,7 +152,8 @@ public void QueryEntitiesWithQueryOptionsUsingODataClientAndVerifyResults() Assert.True(result.All(s => s.SkinColor == Color.Red)); Assert.NotNull(result.SingleOrDefault(r => r.ProductID == 5 && r.UserAccess == AccessLevel.None)); Assert.NotNull(result.SingleOrDefault(r => r.ProductID == 7 && r.UserAccess == AccessLevel.Read)); - Assert.NotNull(result.SingleOrDefault(r => r.ProductID == 8 && r.UserAccess == AccessLevel.Execute)); + Assert.NotNull(result.SingleOrDefault(r => r.ProductID == 8 && + (r.UserAccess == (AccessLevel.Write | AccessLevel.Execute | AccessLevel.Read) || r.UserAccess == (AccessLevel.ReadWrite | AccessLevel.Execute)))); // Atom queryable = _context.CreateQuery("Products") @@ -159,7 +163,7 @@ public void QueryEntitiesWithQueryOptionsUsingODataClientAndVerifyResults() result = queryable.ToList(); Assert.Equal(result.Select(s => s.ProductID).Distinct().Count(), result.Count); Assert.True(result.All(s => s.SkinColor == Color.Red)); - Assert.Contains(result, s => s.UserAccess == AccessLevel.Execute); + Assert.Contains(result, s => (s.UserAccess == (AccessLevel.Write | AccessLevel.Execute | AccessLevel.Read) || s.UserAccess == (AccessLevel.ReadWrite | AccessLevel.Execute))); Assert.Contains(result, s => s.UserAccess == AccessLevel.Read); } @@ -345,6 +349,129 @@ public void QueryEntitiesAndSelectMultiplePropertiesUsingODataClientAndVerifyRes } } + [Fact] + public void QueryEntitiesWithCombinedFlagStringFormatUsingODataClient() + { + // Arrange + _context.Format.UseJson(_model); + + // Act + var queryable = _context.Products + .Where(p => p.UserAccess == AccessLevel.ReadWrite) as DataServiceQuery; + Assert.NotNull(queryable); + var result = queryable.ToList(); + + // Assert + Assert.EndsWith("/Products?$filter=UserAccess eq Microsoft.OData.E2E.TestCommon.Common.Server.Default.AccessLevel'ReadWrite'", queryable.RequestUri.OriginalString); + Assert.All(result, p => Assert.Equal(AccessLevel.ReadWrite, p.UserAccess)); + } + + [Fact] + public void QueryEntitiesWithCombinedFlagNumericRepresentationUsingODataClient() + { + // Arrange + _context.Format.UseJson(_model); + + // Act + var queryable = _context.Products + .Where(p => (int)p.UserAccess == 3) as DataServiceQuery; + Assert.NotNull(queryable); + var result = queryable.ToList(); + + // Assert + Assert.EndsWith("/Products?$filter=UserAccess eq Microsoft.OData.E2E.TestCommon.Common.Server.Default.AccessLevel'ReadWrite'", queryable.RequestUri.OriginalString); + Assert.All(result, p => Assert.Equal(AccessLevel.ReadWrite, p.UserAccess)); + } + + [Fact] + public void QueryEntitiesWithHasOperatorForCombinedFlagsUsingODataClient() + { + // Arrange + _context.Format.UseJson(_model); + + // Act + var queryable = _context.Products + .Where(p => p.UserAccess.Value.HasFlag(AccessLevel.Read) && p.UserAccess.Value.HasFlag(AccessLevel.Write)) as DataServiceQuery; + Assert.NotNull(queryable); + var result = queryable.ToList(); + + // Assert + Assert.EndsWith("/Products?$filter=UserAccess has Microsoft.OData.E2E.TestCommon.Common.Server.Default.AccessLevel'Read' and UserAccess has Microsoft.OData.E2E.TestCommon.Common.Server.Default.AccessLevel'Write'", queryable.RequestUri.OriginalString); + Assert.All(result, p => Assert.True(p.UserAccess.Value.HasFlag(AccessLevel.Read) && p.UserAccess.Value.HasFlag(AccessLevel.Write))); + } + + [Fact] + public void QueryEntitiesWithInOperatorForFlagsUsingODataClient() + { + // Arrange + _context.Format.UseJson(_model); + + // Act + var queryable = _context.Products + .Where(p => new[] { AccessLevel.Read, AccessLevel.Write, AccessLevel.ReadWrite }.Contains(p.UserAccess.Value)) as DataServiceQuery; + Assert.NotNull(queryable); + var result = queryable.ToList(); + + // Assert + Assert.EndsWith("/Products?$filter=UserAccess in ('Read','Write','ReadWrite')", queryable.RequestUri.OriginalString); + Assert.All(result, p => new[] { AccessLevel.Read, AccessLevel.Write, AccessLevel.ReadWrite }.Contains(p.UserAccess.Value)); + } + + [Fact] + public void QueryEntitiesWithCombinedFlagNumericEqualityUsingODataClient() + { + // Arrange + _context.Format.UseJson(_model); + + // Act + // ReadWrite = 3 + var queryable = _context.Products + .Where(p => (int)p.UserAccess == 3) as DataServiceQuery; + Assert.NotNull(queryable); + var result = queryable.ToList(); + + // Assert + Assert.EndsWith("/Products?$filter=UserAccess eq Microsoft.OData.E2E.TestCommon.Common.Server.Default.AccessLevel'ReadWrite'", queryable.RequestUri.OriginalString); + Assert.All(result, p => Assert.Equal(AccessLevel.ReadWrite, p.UserAccess)); + } + + [Fact] + public void QueryEntitiesWithCombinedFlagNumericGreaterThanUsingODataClient() + { + // Arrange + _context.Format.UseJson(_model); + + // Act + // Execute = 4, so this will match Execute and any higher value if present + var queryable = _context.Products + .Where(p => (int)p.UserAccess > 3) as DataServiceQuery; + Assert.NotNull(queryable); + var result = queryable.ToList(); + + // Assert + Assert.EndsWith("/Products?$filter=UserAccess gt Microsoft.OData.E2E.TestCommon.Common.Server.Default.AccessLevel'ReadWrite'", queryable.RequestUri.OriginalString); + Assert.All(result, p => Assert.True((int)p.UserAccess > 3)); + } + + [Fact] + public void QueryEntitiesWithCombinedFlagNumericInOperatorUsingODataClient() + { + // Arrange + _context.Format.UseJson(_model); + + // Act + // None = 0, Read = 1, Write = 2, ReadWrite = 3, Execute = 4 + var validValues = new[] { 0, 3, 4 }; + var queryable = _context.Products + .Where(p => validValues.Contains((int)p.UserAccess)) as DataServiceQuery; + Assert.NotNull(queryable); + var result = queryable.ToList(); + + // Assert + Assert.EndsWith("/Products?$filter=UserAccess in (0,3,4)", queryable.RequestUri.OriginalString, StringComparison.Ordinal); + Assert.All(result, p => Assert.Contains((int)p.UserAccess, validValues)); + } + #region Private methods private void ResetDefaultDataSource() diff --git a/test/EndToEndTests/Tests/Core/Microsoft.OData.Core.E2E.Tests/EnumerationTypeTests/EnumerationTypeQueryTests.cs b/test/EndToEndTests/Tests/Core/Microsoft.OData.Core.E2E.Tests/EnumerationTypeTests/EnumerationTypeQueryTests.cs index 54a3042237..6d2f98f5ff 100644 --- a/test/EndToEndTests/Tests/Core/Microsoft.OData.Core.E2E.Tests/EnumerationTypeTests/EnumerationTypeQueryTests.cs +++ b/test/EndToEndTests/Tests/Core/Microsoft.OData.Core.E2E.Tests/EnumerationTypeTests/EnumerationTypeQueryTests.cs @@ -58,8 +58,6 @@ public EnumerationTypeQueryTests(TestWebApplicationFactory fixture private static string NameSpacePrefix = typeof(DefaultEdmModel).Namespace ?? "Microsoft.OData.E2E.TestCommon.Common.Server.Default"; // Constants - private const string MimeTypeODataParameterFullMetadata = MimeTypes.ApplicationJson + MimeTypes.ODataParameterFullMetadata; - private const string MimeTypeODataParameterMinimalMetadata = MimeTypes.ApplicationJson + MimeTypes.ODataParameterMinimalMetadata; private const string MimeTypeApplicationAtomXml = MimeTypes.ApplicationAtomXml; #region Tests querying entity set and verifies the enumeration type properties @@ -264,7 +262,7 @@ public async Task QueryEntitiesWithEnumFilterAndVerifyResults(string mimeType) List entries = await TestsHelper.QueryResourceSetsAsync(queryText, mimeType); // Assert - Assert.Equal(3, entries.Count); + Assert.Equal(4, entries.Count); var entity0 = entries[0].Properties.ToDictionary(p => p.Name, p => p as ODataProperty); Assert.Equal(6, entity0["ProductID"]?.Value); @@ -279,10 +277,16 @@ public async Task QueryEntitiesWithEnumFilterAndVerifyResults(string mimeType) Assert.Equal("Read", (entity1["UserAccess"]?.Value as ODataEnumValue)?.Value); var entity2 = entries[2].Properties.ToDictionary(p => p.Name, p => p as ODataProperty); - Assert.Equal(9, entity2["ProductID"]?.Value); - Assert.Equal("Computer", entity2["Name"]?.Value); - Assert.Equal("Green", (entity2["SkinColor"]?.Value as ODataEnumValue)?.Value); - Assert.Equal("Read", (entity2["UserAccess"]?.Value as ODataEnumValue)?.Value); + Assert.Equal(8, entity2["ProductID"]?.Value); + Assert.Equal("Car", entity2["Name"]?.Value); + Assert.Equal("Red", (entity2["SkinColor"]?.Value as ODataEnumValue)?.Value); + Assert.Equal("ReadWrite, Execute", (entity2["UserAccess"]?.Value as ODataEnumValue)?.Value); + + var entity3 = entries[3].Properties.ToDictionary(p => p.Name, p => p as ODataProperty); + Assert.Equal(9, entity3["ProductID"]?.Value); + Assert.Equal("Computer", entity3["Name"]?.Value); + Assert.Equal("Green", (entity3["SkinColor"]?.Value as ODataEnumValue)?.Value); + Assert.Equal("Read", (entity3["UserAccess"]?.Value as ODataEnumValue)?.Value); } #endregion @@ -336,6 +340,217 @@ public async Task QueryEntitiesAndSelectEnumProperties(string mimeType) #endregion + #region Tests Flags Enums and verifies the results. + + [Theory] + [InlineData("UserAccess has Microsoft.OData.E2E.TestCommon.Common.Server.Default.AccessLevel'Read'", 4)] + [InlineData("UserAccess has Microsoft.OData.E2E.TestCommon.Common.Server.Default.AccessLevel'Execute'", 1)] + [InlineData("UserAccess has Microsoft.OData.E2E.TestCommon.Common.Server.Default.AccessLevel'ReadWrite'", 2)] + [InlineData("UserAccess has Microsoft.OData.E2E.TestCommon.Common.Server.Default.AccessLevel'Read, Write'", 2)] + [InlineData("UserAccess has Microsoft.OData.E2E.TestCommon.Common.Server.Default.AccessLevel'Read, Write, Execute'", 1)] + public async Task QueryFlagsEnumAndVerifyResults_WithHasOperator(string filter, int expectedCount) + { + // Arrange + var queryText = $"Products?$filter={filter}"; + + // Act + List entries = await TestsHelper.QueryResourceSetsAsync(queryText, MimeTypeODataParameterFullMetadata); + + // Assert + Assert.Equal(expectedCount, entries.Count); + } + + [Theory] + [InlineData("UserAccess in (0,3,4)", new object[] { AccessLevel.None, AccessLevel.ReadWrite })] + [InlineData("UserAccess in ('1',3,'4')", new object[] { AccessLevel.Read, AccessLevel.ReadWrite, AccessLevel.Read })] + [InlineData("UserAccess in ('Read', 'ReadWrite','Execute')", new object[] { AccessLevel.Read, AccessLevel.ReadWrite, AccessLevel.Read })] + [InlineData("UserAccess in (Microsoft.OData.E2E.TestCommon.Common.Server.Default.AccessLevel'Read', Microsoft.OData.E2E.TestCommon.Common.Server.Default.AccessLevel'ReadWrite')", new object[] { AccessLevel.Read, AccessLevel.ReadWrite, AccessLevel.Read })] + [InlineData("UserAccess in ( Microsoft.OData.E2E.TestCommon.Common.Server.Default.AccessLevel'Read', Microsoft.OData.E2E.TestCommon.Common.Server.Default.AccessLevel'ReadWrite')", new object[] { AccessLevel.Read, AccessLevel.ReadWrite, AccessLevel.Read })] + [InlineData("UserAccess in ('Read', 'Read, Write, Execute','Execute')", new object[] { AccessLevel.Read, AccessLevel.ReadWrite | AccessLevel.Execute, AccessLevel.Read })] + [InlineData("UserAccess in ('Read', 'Read, Write, Execute','Execute')", new object[] { AccessLevel.Read, AccessLevel.ReadWrite | AccessLevel.Execute, AccessLevel.Read })] + [InlineData("UserAccess in ('None', 'ReadWrite, Execute','Execute')", new object[] { AccessLevel.None, AccessLevel.ReadWrite | AccessLevel.Execute })] + [InlineData("UserAccess in ('None', ' ReadWrite, Execute', 'Execute')", new object[] { AccessLevel.None, AccessLevel.ReadWrite | AccessLevel.Execute })] + [InlineData("UserAccess in ('Read', 'ReadWrite, Execute')", new object[] { AccessLevel.Read, AccessLevel.ReadWrite | AccessLevel.Execute, AccessLevel.Read })] + public async Task QueryFlagsEnumAndVerifyResults_WithInOperator(string filter, object[] expectedUserAccess) + { + // Arrange + var queryText = $"Products?$filter={filter}"; + + // Act + List entries = await TestsHelper.QueryResourceSetsAsync(queryText, MimeTypeODataParameterMinimalMetadata); + + // Assert + Assert.Equal(expectedUserAccess.Length, entries.Count); + + var userAccessValues = entries.Select(e => e.Properties.Single(p => p.Name == "UserAccess") as ODataProperty).Select(p => (p.Value as ODataEnumValue)?.Value); + Assert.All(expectedUserAccess, ua => Assert.Contains(ua.ToString(), userAccessValues)); + } + + [Fact] + public async Task QueryFlagsEnumAndVerifyResults_InOperator_StringNumbers() + { + var filter = @"UserAccess in (""1"",3,""4"")"; + var expectedUserAccess = new object[] { AccessLevel.Read, AccessLevel.ReadWrite, AccessLevel.Read }; + + var queryText = $"Products?$filter={filter}"; + List entries = await TestsHelper.QueryResourceSetsAsync(queryText, MimeTypeODataParameterMinimalMetadata); + + Assert.Equal(expectedUserAccess.Length, entries.Count); + var userAccessValues = entries.Select(e => e.Properties.Single(p => p.Name == "UserAccess") as ODataProperty) + .Select(p => (p.Value as ODataEnumValue)?.Value); + Assert.All(expectedUserAccess, ua => Assert.Contains(ua.ToString(), userAccessValues)); + } + + [Fact] + public async Task QueryFlagsEnumAndVerifyResults_InOperator_WorksWithMixedDoubleAndSingleQuotes() + { + var filter = @"UserAccess in (""Read"", 'ReadWrite',""Execute"")"; + var expectedUserAccess = new object[] { AccessLevel.Read, AccessLevel.ReadWrite, AccessLevel.Read }; + + var queryText = $"Products?$filter={filter}"; + List entries = await TestsHelper.QueryResourceSetsAsync(queryText, MimeTypeODataParameterMinimalMetadata); + + Assert.Equal(expectedUserAccess.Length, entries.Count); + var userAccessValues = entries.Select(e => e.Properties.Single(p => p.Name == "UserAccess") as ODataProperty) + .Select(p => (p.Value as ODataEnumValue)?.Value); + Assert.All(expectedUserAccess, ua => Assert.Contains(ua.ToString(), userAccessValues)); + } + + [Fact] + public async Task QueryFlagsEnumAndVerifyResults_InOperator_FullQualifiedNameWorksWithDoubleQuotes() + { + var filter = @"UserAccess in (Microsoft.OData.E2E.TestCommon.Common.Server.Default.AccessLevel""Read"", Microsoft.OData.E2E.TestCommon.Common.Server.Default.AccessLevel""ReadWrite"")"; + var expectedUserAccess = new object[] { AccessLevel.Read, AccessLevel.ReadWrite, AccessLevel.Read }; + + var queryText = $"Products?$filter={filter}"; + List entries = await TestsHelper.QueryResourceSetsAsync(queryText, MimeTypeODataParameterMinimalMetadata); + + Assert.Equal(expectedUserAccess.Length, entries.Count); + var userAccessValues = entries.Select(e => e.Properties.Single(p => p.Name == "UserAccess") as ODataProperty) + .Select(p => (p.Value as ODataEnumValue)?.Value); + Assert.All(expectedUserAccess, ua => Assert.Contains(ua.ToString(), userAccessValues)); + } + + [Fact] + public async Task QueryFlagsEnumAndVerifyResults_InOperator_WorksWithDoubleQuotes() + { + var filter = @"UserAccess in (""None"", "" ReadWrite, Execute"", ""Execute"")"; + var expectedUserAccess = new object[] { AccessLevel.None, AccessLevel.ReadWrite | AccessLevel.Execute }; + + var queryText = $"Products?$filter={filter}"; + List entries = await TestsHelper.QueryResourceSetsAsync(queryText, MimeTypeODataParameterMinimalMetadata); + + Assert.Equal(expectedUserAccess.Length, entries.Count); + var userAccessValues = entries.Select(e => e.Properties.Single(p => p.Name == "UserAccess") as ODataProperty) + .Select(p => (p.Value as ODataEnumValue)?.Value); + Assert.All(expectedUserAccess, ua => Assert.Contains(ua.ToString(), userAccessValues)); + } + + [Fact] + public async Task QueryFlagsEnumAndVerifyResults_InOperator_MissingDoubleQuote1() + { + var filter = @"UserAccess in ('Read', ""ReadWrite, Execute)"; + var queryText = $"Products?$filter={filter}"; + ODataMessageReaderSettings readerSettings = new() { BaseUri = _baseUri }; + var requestUrl = new Uri(_baseUri.AbsoluteUri + queryText, UriKind.Absolute); + + var requestMessage = new TestHttpClientRequestMessage(requestUrl, Client); + requestMessage.SetHeader("Accept", MimeTypeODataParameterMinimalMetadata); + + var responseMessage = await requestMessage.GetResponseAsync(); + + Assert.Equal(400, responseMessage.StatusCode); + } + + [Fact] + public async Task QueryFlagsEnumAndVerifyResults_InOperator_MissingDoubleQuote2() + { + var filter = @"UserAccess in (""Read, ""ReadWrite, Execute"")"; + var queryText = $"Products?$filter={filter}"; + ODataMessageReaderSettings readerSettings = new() { BaseUri = _baseUri }; + var requestUrl = new Uri(_baseUri.AbsoluteUri + queryText, UriKind.Absolute); + + var requestMessage = new TestHttpClientRequestMessage(requestUrl, Client); + requestMessage.SetHeader("Accept", MimeTypeODataParameterMinimalMetadata); + + var responseMessage = await requestMessage.GetResponseAsync(); + + Assert.Equal(400, responseMessage.StatusCode); + } + + [Fact] + public async Task QueryFlagsEnumAndVerifyResults_InOperator_MissingSingleQuote1() + { + var filter = @"UserAccess in ('Read, 'ReadWrite, Execute')"; + var queryText = $"Products?$filter={filter}"; + ODataMessageReaderSettings readerSettings = new() { BaseUri = _baseUri }; + var requestUrl = new Uri(_baseUri.AbsoluteUri + queryText, UriKind.Absolute); + + var requestMessage = new TestHttpClientRequestMessage(requestUrl, Client); + requestMessage.SetHeader("Accept", MimeTypeODataParameterMinimalMetadata); + + var responseMessage = await requestMessage.GetResponseAsync(); + + Assert.Equal(400, responseMessage.StatusCode); + } + + [Fact] + public async Task QueryFlagsEnumAndVerifyResults_InOperator_MissingSingleQuote2() + { + var filter = @"UserAccess in ('Read', ReadWrite, Execute')"; + var queryText = $"Products?$filter={filter}"; + ODataMessageReaderSettings readerSettings = new() { BaseUri = _baseUri }; + var requestUrl = new Uri(_baseUri.AbsoluteUri + queryText, UriKind.Absolute); + + var requestMessage = new TestHttpClientRequestMessage(requestUrl, Client); + requestMessage.SetHeader("Accept", MimeTypeODataParameterMinimalMetadata); + + var responseMessage = await requestMessage.GetResponseAsync(); + + Assert.Equal(400, responseMessage.StatusCode); + } + + [Fact] + public async Task QueryFlagsEnumAndVerifyResults_InOperator_MissingSingleQuote3() + { + var filter = @"UserAccess in (Read', ReadWrite, Execute')"; + var queryText = $"Products?$filter={filter}"; + ODataMessageReaderSettings readerSettings = new() { BaseUri = _baseUri }; + var requestUrl = new Uri(_baseUri.AbsoluteUri + queryText, UriKind.Absolute); + + var requestMessage = new TestHttpClientRequestMessage(requestUrl, Client); + requestMessage.SetHeader("Accept", MimeTypeODataParameterMinimalMetadata); + + var responseMessage = await requestMessage.GetResponseAsync(); + + Assert.Equal(400, responseMessage.StatusCode); + } + + [Theory] + [InlineData("UserAccess eq 3", 1)] + [InlineData("UserAccess eq '3'", 1)] + [InlineData("UserAccess eq 'ReadWrite'", 1)] + [InlineData("UserAccess eq Microsoft.OData.E2E.TestCommon.Common.Server.Default.AccessLevel'ReadWrite'", 1)] + [InlineData("UserAccess ne 1", 3)] + [InlineData("UserAccess ne 'Read'", 3)] + [InlineData("UserAccess ne Microsoft.OData.E2E.TestCommon.Common.Server.Default.AccessLevel'Read'", 3)] + [InlineData("UserAccess ne 0", 4)] + [InlineData("UserAccess ne 'None'", 4)] + [InlineData("UserAccess ne Microsoft.OData.E2E.TestCommon.Common.Server.Default.AccessLevel'None'", 4)] + public async Task QueryFlagsEnumAndVerifyResults_WithSeveralOperators(string filter, int expectedCount) + { + // Arrange + var queryText = $"Products?$filter={filter}"; + + // Act + List entries = await TestsHelper.QueryResourceSetsAsync(queryText, MimeTypeODataParameterMinimalMetadata); + + // Assert + Assert.Equal(expectedCount, entries.Count); + } + + #endregion + #region Private methods private EnumerationTypeQueryTestsHelper TestsHelper { diff --git a/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/Binders/MetadataBindingUtilsTests.cs b/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/Binders/MetadataBindingUtilsTests.cs index b3c6097082..41d81d4719 100644 --- a/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/Binders/MetadataBindingUtilsTests.cs +++ b/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/Binders/MetadataBindingUtilsTests.cs @@ -214,6 +214,85 @@ public void IfTypePromotionNeeded_SourceIsFloatMemberValuesInStringAndTargetIsEn } } + [Theory] + [InlineData("FullTime")] + [InlineData("PartTime")] + [InlineData("Contractor")] + [InlineData("Temporary")] + [InlineData("FullTime, PartTime")] + [InlineData("FullTime, Temporary")] + [InlineData("Permanent, Temporary")] + [InlineData("Intern, Temporary")] + [InlineData("Permanent")] + [InlineData("PartTime, Contractor, Temporary")] + public void IfTypePromotionNeeded_SourceIsFlagsCompositeMemberNameOrComposite_ConstantNodeIsCreated(string enumValue) + { + // Arrange + SingleValueNode source = new ConstantNode(enumValue); + IEdmTypeReference targetTypeReference = new EdmEnumTypeReference(EmployeeTypeWithFlags, false); + // Act + ConstantNode result = MetadataBindingUtils.ConvertToTypeIfNeeded(source, targetTypeReference) as ConstantNode; + + // Assert + result.ShouldBeEnumNode(EmployeeTypeWithFlags, enumValue); + Assert.Equal(enumValue, result.Value.ToString()); + } + + [Theory] + [InlineData("fulltime", "FullTime")] + [InlineData("FULLTIME", "FullTime")] + [InlineData("parttime", "PartTime")] + [InlineData("PARTTIME", "PartTime")] + [InlineData("contractor", "Contractor")] + [InlineData("temporary", "Temporary")] + [InlineData("fulltime, parttime", "FullTime, PartTime")] + [InlineData("fulltime, temporary", "FullTime, Temporary")] + [InlineData("permanent, temporary", "Permanent, Temporary")] + [InlineData("intern, temporary", "Intern, Temporary")] + [InlineData("permanent", "Permanent")] + [InlineData("parttime, contractor, temporary", "PartTime, Contractor, Temporary")] + [InlineData("PARTTIME, contractor, Temporary", "PartTime, Contractor, Temporary")] + public void IfTypePromotionNeeded_SourceIsFlagsCompositeMemberNameOrComposite_enableCaseInsensitive_ConstantNodeIsCreated(string enumValue, string expected) + { + // Arrange + SingleValueNode source = new ConstantNode(enumValue); + IEdmTypeReference targetTypeReference = new EdmEnumTypeReference(EmployeeTypeWithFlags, false); + + // Act + ConstantNode result = MetadataBindingUtils.ConvertToTypeIfNeeded(source, targetTypeReference, true) as ConstantNode; + + // Assert + result.ShouldBeEnumNode(EmployeeTypeWithFlags, expected); + Assert.Equal(expected, result.Value.ToString()); + } + + [Theory] + [InlineData(2, "FullTime")] + [InlineData("2", "FullTime")] + [InlineData(6, "Permanent")] + [InlineData("6", "Permanent")] + [InlineData(8, "Contractor")] + [InlineData("8", "Contractor")] + [InlineData(28, "Temporary")] + [InlineData("28", "Temporary")] + [InlineData(26, "FullTime, Contractor, Intern")] + [InlineData("26", "FullTime, Contractor, Intern")] + [InlineData(30, "FullTime, Temporary")] + [InlineData("22", "Permanent, Intern")] + [InlineData(22, "Permanent, Intern")] + public void IfTypePromotionNeeded_SourceIsFlagsIntegralValues_ConstantNodeIsCreated(object enumValue, string expectedLiteralValue) + { + // Arrange + SingleValueNode source = new ConstantNode(enumValue); + IEdmTypeReference targetTypeReference = new EdmEnumTypeReference(EmployeeTypeWithFlags, false); + // Act + ConstantNode result = MetadataBindingUtils.ConvertToTypeIfNeeded(source, targetTypeReference) as ConstantNode; + + // Assert + result.ShouldBeEnumNode(EmployeeTypeWithFlags, expectedLiteralValue); + Assert.Equal(expectedLiteralValue, result.Value.ToString()); + } + private static EdmEnumType WeekDayEmumType { get @@ -243,6 +322,23 @@ private static EdmEnumType EmployeeType return employeeType; } } + + private static EdmEnumType EmployeeTypeWithFlags + { + get + { + EdmEnumType employeeType = new EdmEnumType("NS", "EmployeeTypeWithFlags", isFlags: true); + employeeType.AddMember("FullTime", new EdmEnumMemberValue((long)2)); + employeeType.AddMember("PartTime", new EdmEnumMemberValue((long)4)); + employeeType.AddMember("Permanent", new EdmEnumMemberValue((long)(2 | 4))); // FullTime | PartTime = 6 + employeeType.AddMember("Contractor", new EdmEnumMemberValue((long)8)); + employeeType.AddMember("Intern", new EdmEnumMemberValue((long)16)); + employeeType.AddMember("Temporary", new EdmEnumMemberValue((long) (4 | 8 | 16))); // PartTime | Contractor | Intern = 28 + + return employeeType; + } + } + #endregion } }