From bd08585374f3b1c421b3f6f10a2915934699b14b Mon Sep 17 00:00:00 2001 From: Samuel Wanjohi Date: Tue, 23 Jul 2024 12:27:04 +0300 Subject: [PATCH] Add support for Enums as Key in OData Client and Allow $filter with integer value in squote and without squote (#3018) * Support Enums as Keys in OData Client (#3013) * Checkout from 'main' * Add and modify test cases * Only allow nullable Generic Type Enum * Adding a test that throws Exception if key is nullable generic type with struct as generic argument * Adding a test to mock querying data with enum as keys * Add a test to select single entity * Add test to filter by enum member name and a test to use ByKey to filter by composite keys * Add a test to filter by enum key only using ByKey() * Allow $filtering with enum integer value in squote and without squote (#3014) * Allow comparison with underlying integral for Enum member and add capability to convert integral value to enum member name * Bump with tests * Move helper methods to EnumHelper, use them to check for enum values and adds more tests to handle float, long and int. * Use explicit/exact type instead of 'var' * Add more tests to handle huge, long values * Add assertion for comparing enum member name with the result * Move extension methods from Edm EnumHelper.cs to Core in EdmExtensionMethods.cs --- .../Metadata/ODataTypeInfo.cs | 4 +- .../EdmExtensionMethods.cs | 42 +++ .../UriParser/Binders/MetadataBindingUtils.cs | 17 +- .../UriParser/TypePromotionUtils.cs | 7 +- .../Metadata/ClientTypeUtilTests.cs | 145 ++++++++ .../Tracking/DataServiceContextQueryTests.cs | 321 ++++++++++++++++++ .../Binders/MetadataBindingUtilsTests.cs | 194 +++++++++++ .../UriParser/TypePromotionUtilsTests.cs | 18 + 8 files changed, 739 insertions(+), 9 deletions(-) create mode 100644 test/FunctionalTests/Microsoft.OData.Client.Tests/Tracking/DataServiceContextQueryTests.cs diff --git a/src/Microsoft.OData.Client/Metadata/ODataTypeInfo.cs b/src/Microsoft.OData.Client/Metadata/ODataTypeInfo.cs index d94e62c75e..9b8666c585 100644 --- a/src/Microsoft.OData.Client/Metadata/ODataTypeInfo.cs +++ b/src/Microsoft.OData.Client/Metadata/ODataTypeInfo.cs @@ -321,7 +321,9 @@ private PropertyInfo[] GetKeyProperties() throw c.Error.InvalidOperation(c.Strings.ClientType_KeysOnDifferentDeclaredType(typeName)); } - if (!PrimitiveType.IsKnownType(key.PropertyType) && !(key.PropertyType.GetGenericTypeDefinition() == typeof(System.Nullable<>) && key.PropertyType.GetGenericArguments().First().IsEnum())) + // Check if the key property's type is a known primitive, an enum, or a nullable generic. + // If it doesn't meet any of these conditions, throw an InvalidOperationException. + if (!PrimitiveType.IsKnownType(key.PropertyType) && !key.PropertyType.IsEnum() && !(key.PropertyType.IsGenericType() && key.PropertyType.GetGenericTypeDefinition() == typeof(System.Nullable<>) && key.PropertyType.GetGenericArguments().First().IsEnum())) { throw c.Error.InvalidOperation(c.Strings.ClientType_KeysMustBeSimpleTypes(key.Name, typeName, key.PropertyType.FullName)); } diff --git a/src/Microsoft.OData.Core/EdmExtensionMethods.cs b/src/Microsoft.OData.Core/EdmExtensionMethods.cs index e42c146530..9a99491efb 100644 --- a/src/Microsoft.OData.Core/EdmExtensionMethods.cs +++ b/src/Microsoft.OData.Core/EdmExtensionMethods.cs @@ -122,5 +122,47 @@ public static bool HasKey(IEdmNavigationSource currentNavigationSource, IEdmStru return false; } + + /// + /// Parse an enum integral value to enum member. + /// + /// edm enum type + /// input integral value. + /// parsed result. + /// true if parse succeeds, false if parse fails. + public static bool TryParse(this IEdmEnumType enumType, long value, out IEdmEnumMember enumMember) + { + enumMember = null; + foreach (IEdmEnumMember member in enumType.Members) + { + if (member.Value.Value == value) + { + enumMember = member; + return true; + } + } + + return false; + } + + /// + /// Checks if the given member name exists in the enum type. + /// + /// The enum type. + /// The member name to check. + /// 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, StringComparison comparison = StringComparison.Ordinal) + { + foreach (IEdmEnumMember member in enumType.Members) + { + if (string.Equals(member.Name, memberName, comparison)) + { + return true; + } + } + + return false; + } } } diff --git a/src/Microsoft.OData.Core/UriParser/Binders/MetadataBindingUtils.cs b/src/Microsoft.OData.Core/UriParser/Binders/MetadataBindingUtils.cs index 3ed69bfb3f..e2aed8d46b 100644 --- a/src/Microsoft.OData.Core/UriParser/Binders/MetadataBindingUtils.cs +++ b/src/Microsoft.OData.Core/UriParser/Binders/MetadataBindingUtils.cs @@ -58,19 +58,26 @@ internal static SingleValueNode ConvertToTypeIfNeeded(SingleValueNode source, IE } ConstantNode constantNode = source as ConstantNode; - if (constantNode != null && constantNode.Value != null && source.TypeReference.IsString() && targetTypeReference.IsEnum()) + // Check if the source node is a constant node, not null, and the source type is either string or integral + // and the target type is an enum. + if (constantNode != null && constantNode.Value != null && (source.TypeReference.IsString() || source.TypeReference.IsIntegral()) && targetTypeReference.IsEnum()) { string memberName = constantNode.Value.ToString(); IEdmEnumType enumType = targetTypeReference.Definition as IEdmEnumType; - if (enumType.Members.Any(m => string.Compare(m.Name, memberName, StringComparison.Ordinal) == 0)) + if(enumType.ContainsMember(memberName, StringComparison.Ordinal)) { string literalText = ODataUriUtils.ConvertToUriLiteral(constantNode.Value, default(ODataVersion)); - return new ConstantNode(new ODataEnumValue(constantNode.Value.ToString(), targetTypeReference.Definition.ToString()), literalText, targetTypeReference); + return new ConstantNode(new ODataEnumValue(memberName, enumType.ToString()), literalText, targetTypeReference); } - else + + // 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)) { - throw new ODataException(ODataErrorStrings.Binder_IsNotValidEnumConstant(memberName)); + string literalText = ODataUriUtils.ConvertToUriLiteral(enumMember.Name, default(ODataVersion)); + return new ConstantNode(new ODataEnumValue(enumMember.Name, enumType.ToString()), literalText, targetTypeReference); } + + throw new ODataException(ODataErrorStrings.Binder_IsNotValidEnumConstant(memberName)); } if (!TypePromotionUtils.CanConvertTo(source, source.TypeReference, targetTypeReference)) diff --git a/src/Microsoft.OData.Core/UriParser/TypePromotionUtils.cs b/src/Microsoft.OData.Core/UriParser/TypePromotionUtils.cs index 3ed60a4c24..69b76d3bc8 100644 --- a/src/Microsoft.OData.Core/UriParser/TypePromotionUtils.cs +++ b/src/Microsoft.OData.Core/UriParser/TypePromotionUtils.cs @@ -267,14 +267,15 @@ internal static bool PromoteOperandTypes( return true; } - // Comparing an enum with a string is valid - if (left != null && right != null && left.IsEnum() && right.IsString()) + // Comparing an enum with a string or int is valid + if (left != null && right != null && left.IsEnum() && (right.IsString() || right.IsIntegral())) { right = left; return true; } - if (left != null && right != null && right.IsEnum() && left.IsString()) + // Comparing an enum with a string or int is valid + if (left != null && right != null && right.IsEnum() && (left.IsString() || left.IsIntegral())) { left = right; return true; diff --git a/test/FunctionalTests/Microsoft.OData.Client.Tests/Metadata/ClientTypeUtilTests.cs b/test/FunctionalTests/Microsoft.OData.Client.Tests/Metadata/ClientTypeUtilTests.cs index 3fa39e6981..db997409d5 100644 --- a/test/FunctionalTests/Microsoft.OData.Client.Tests/Metadata/ClientTypeUtilTests.cs +++ b/test/FunctionalTests/Microsoft.OData.Client.Tests/Metadata/ClientTypeUtilTests.cs @@ -6,6 +6,8 @@ using Microsoft.OData.Client.Metadata; using System; +using System.Linq; +using System.Reflection; using Xunit; namespace Microsoft.OData.Client.Tests.Metadata @@ -50,6 +52,99 @@ public void IFTypeProperty_HasKeyAttributeAndOneProperty_TypeIsEntityAndDoesNotT Assert.True(actualResult); } + [Fact] + public void IFType_HasMultipleKeyAttributesWhereOneIsEnum_TypeIsEntityAndDoesNotThrowException() + { + //Arrange + Type employee = typeof(Employee); + + //Act + bool actualResult = ClientTypeUtil.TypeOrElementTypeIsEntity(employee); + + //Assert + Assert.True(actualResult); + } + + [Fact] + public void IFTypeProperty_HasMultipleKeyAttributes_GetKeyPropertiesOnType_DoesNotThrowException() + { + //Arrange + Type employee = typeof(Employee); + + int expectedNumberOfKeyProperties = 4; // 2 Primitive Known Types, 1 Enum Type, 1 Enum Nullable Generic Type + + //Act + PropertyInfo[] keyProperties = ClientTypeUtil.GetKeyPropertiesOnType(employee); + + //Assert + Assert.Equal(expectedNumberOfKeyProperties, keyProperties.Length); + } + + [Fact] + public void IFTypeProperty_HasEnumTypeKeyAttribute_GetKeyPropertiesOnType_DoesNotThrowException() + { + // Arrange + Type employee = typeof(Employee); + + //Act + PropertyInfo[] keyProperties = ClientTypeUtil.GetKeyPropertiesOnType(employee); + PropertyInfo key = keyProperties.Single(k => k.Name == "EmpType"); + + //Assert + Assert.True(key.PropertyType.IsEnum()); + Assert.True(key.PropertyType == typeof(EmployeeType)); + } + + [Fact] + public void IFTypeProperty_HasKnownPrimitiveTypesKeyAttributes_GetKeyPropertiesOnType_DoesNotThrowException() + { + // Arrange + Type employee = typeof(Employee); + + //Act + PropertyInfo[] keyProperties = ClientTypeUtil.GetKeyPropertiesOnType(employee); + + PropertyInfo empNumKey = keyProperties.Single(k => k.Name == "EmpNumber"); + PropertyInfo deptNumKey = keyProperties.Single(k => k.Name == "DeptNumber"); + + //Assert + Assert.True(PrimitiveType.IsKnownType(empNumKey.PropertyType) && empNumKey.PropertyType == typeof(int)); + Assert.True(PrimitiveType.IsKnownType(deptNumKey.PropertyType) && deptNumKey.PropertyType == typeof(string)); + } + + [Fact] + public void IFTypeProperty_HasNullableGenericTypeKeyAttribute_OfTypeEnum_GetKeyPropertiesOnType_DoesNotThrowException() + { + // Arrange + Type employee = typeof(Employee); + + //Act + PropertyInfo[] keyProperties = ClientTypeUtil.GetKeyPropertiesOnType(employee); + PropertyInfo key = keyProperties.Single(k => k.Name == "NullableEmpType"); + + //Assert + Assert.True(key.PropertyType.IsGenericType); + Assert.True(key.PropertyType == typeof(System.Nullable)); + } + + [Fact] + public void IFTypeProperty_HasNullableGenericTypeKey_OfTypeStruct_GetKeyPropertiesOnType_ThrowsException() + { + // Arrange + Type employee = typeof(EmployeeWithNullableStruct); + + PropertyInfo empTypeStructKey = employee.GetProperty("EmpTypeStruct"); + + InvalidOperationException expectedException = Error.InvalidOperation(Strings.ClientType_KeysMustBeSimpleTypes(empTypeStructKey.Name, employee.ToString(), empTypeStructKey.PropertyType.FullName)); + + //Act + InvalidOperationException actualException = Assert.Throws(() => ClientTypeUtil.GetKeyPropertiesOnType(employee)); + + //Assert + Assert.NotNull(actualException); + Assert.Equal(expectedException.Message, actualException.Message); + } + public class Person { [System.ComponentModel.DataAnnotations.Key] @@ -69,5 +164,55 @@ public class Car public int NonStandardId { get; set; } } + public class Employee + { + [System.ComponentModel.DataAnnotations.Key] + public int EmpNumber { get; set; } + + [System.ComponentModel.DataAnnotations.Key] + public string DeptNumber { get; set; } + + [System.ComponentModel.DataAnnotations.Key] + public EmployeeType EmpType { get; set; } + + [System.ComponentModel.DataAnnotations.Key] + public EmployeeType? NullableEmpType { get; set; } + + public string Name { get; set; } + + [System.ComponentModel.DataAnnotations.Schema.ForeignKey("DeptNumber")] + public Department Department { get; set; } + } + + public class EmployeeWithNullableStruct + { + [System.ComponentModel.DataAnnotations.Key] + public int EmpNumber { get; set; } + + [System.ComponentModel.DataAnnotations.Key] + public EmployeeTypeStruct? EmpTypeStruct { get; set; } + + public string Name { get; set; } + } + + public enum EmployeeType + { + None = 1, + FullTime = 2, + PartTime = 3 + } + + public class Department + { + [System.ComponentModel.DataAnnotations.Key] + public string DeptId { get; set; } + public string Name { get; set; } + } + + public struct EmployeeTypeStruct + { + public int EmpTypeId { get; set; } + } + } } diff --git a/test/FunctionalTests/Microsoft.OData.Client.Tests/Tracking/DataServiceContextQueryTests.cs b/test/FunctionalTests/Microsoft.OData.Client.Tests/Tracking/DataServiceContextQueryTests.cs new file mode 100644 index 0000000000..46c042354f --- /dev/null +++ b/test/FunctionalTests/Microsoft.OData.Client.Tests/Tracking/DataServiceContextQueryTests.cs @@ -0,0 +1,321 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +using Microsoft.OData.Edm.Csdl; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Xml; +using Xunit; + +namespace Microsoft.OData.Client.Tests.Tracking +{ + public class DataServiceContextQueryTests + { + private const string ServiceRoot = "http://localhost:8007"; + private readonly Container _defaultContext; + + #region Test Edmx + private const string Edmx = @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"; + #endregion + + public DataServiceContextQueryTests() + { + var uri = new Uri(ServiceRoot); + _defaultContext = new Container(uri); + } + + [Fact] + public async Task SelectEntities_WithEnumAsKey_DoNotThrowException() + { + // Arrange + var expectedUri = $"{ServiceRoot}/Employees"; + + string response = @"{ + ""@odata.context"": ""http://localhost:8007/$metadata#Employees"", + ""value"": [ + { + ""EmpNumber"": 1, + ""EmpType"": ""FullTime"", + ""OrgId"": 1, + ""Name"": ""John Doe"" + }, + { + ""EmpNumber"": 2, + ""EmpType"": ""PartTime"", + ""OrgId"": 1, + ""Name"": ""Jane Doe"" + } + ] +}"; + SetupContextWithRequestPipeline(_defaultContext, response, "Employees"); + _defaultContext.SendingRequest2 += (sender, args) => + { + Assert.Equal(expectedUri, args.RequestMessage.Url.ToString()); + }; + + // Act + DataServiceQuery query = _defaultContext.Employees; + IEnumerable employees = await query.ExecuteAsync(); + + // Assert + Assert.Equal(expectedUri, query.ToString()); + Assert.Equal(2, employees.Count()); + } + + [Fact] + public void UseWhereToFilterByOtherKeyOtherThanEnumKey_WithEnumAsKey_DoNotThrowException() + { + // Arrange + string expectedUri = $"{ServiceRoot}/Employees?$filter=EmpNumber eq 8"; + + string response = @"{ + ""@odata.context"": ""http://localhost:8007/$metadata#Employees"", + ""value"": [ + { + ""EmpNumber"": 8, + ""EmpType"": ""PartTime"", + ""OrgId"": 1, + ""Name"": ""Employee Two"" + } + ] +}"; + SetupContextWithRequestPipeline(_defaultContext, response, "Employees"); + _defaultContext.SendingRequest2 += (sender, args) => + { + Assert.Equal($"{expectedUri}&$top=1", args.RequestMessage.Url.ToString()); + }; + + // Act + IQueryable query = _defaultContext.Employees.Where(e => e.EmpNumber == 8); + Employee employee = query.First(); + + // Assert + Assert.Equal(expectedUri, query.ToString()); + Assert.Equal(EmployeeType.PartTime, employee.EmpType); + Assert.Equal("Employee Two", employee.Name); + } + + [Fact] + public void UseWhereToFilterByEnumKey_WithEnumAsKey_DoNotThrowException() + { + // Arrange + string expectedUri = $"{ServiceRoot}/Employees?$filter=EmpType eq Microsoft.OData.Client.Tests.Tracking.EmployeeType'PartTime'"; + + string response = @"{ + ""@odata.context"": ""http://localhost:8007/$metadata#Employees"", + ""value"": [ + { + ""EmpNumber"": 8, + ""EmpType"": ""PartTime"", + ""OrgId"": 1, + ""Name"": ""Employee 45"" + } + ] +}"; + SetupContextWithRequestPipeline(_defaultContext, response, "Employees"); + _defaultContext.SendingRequest2 += (sender, args) => + { + Assert.Equal($"{expectedUri}&$top=1", args.RequestMessage.Url.ToString()); + }; + + // Act + var query = _defaultContext.Employees.Where(e => e.EmpType == EmployeeType.PartTime); + Employee employee = query.First(); + + // Assert + Assert.Equal(expectedUri, query.ToString()); + Assert.Equal(8, employee.EmpNumber); + Assert.Equal(EmployeeType.PartTime, employee.EmpType); + } + + [Fact] + public async Task FilterByCompositeKeys_WithEnumAsKey_DoNotThrowException() + { + // Arrange + string expectedUri = $"{ServiceRoot}/Employees(EmpNumber=8,EmpType=Microsoft.OData.Client.Tests.Tracking.EmployeeType'PartTime',OrgId=1)"; + + string response = @"{ + ""@odata.context"": ""http://localhost:8007/$metadata#Employees"", + ""value"": [ + { + ""EmpNumber"": 8, + ""EmpType"": ""PartTime"", + ""OrgId"": 1, + ""Name"": ""Employee 24"" + } + ] +}"; + SetupContextWithRequestPipeline(_defaultContext, response, "Employees"); + _defaultContext.SendingRequest2 += (sender, args) => + { + Assert.Equal(expectedUri, args.RequestMessage.Url.ToString()); + }; + + // Act + EmployeeSingle query = _defaultContext.Employees.ByKey( + new Dictionary() { { "EmpNumber", 8 }, { "EmpType", EmployeeType.PartTime }, { "OrgId", 1 } }); + + Employee employee = await query.GetValueAsync().ConfigureAwait(false); + + // Assert + Assert.Equal(expectedUri, query.Query.ToString()); + Assert.Equal("Employee 24", employee.Name); + Assert.Equal(EmployeeType.PartTime, employee.EmpType); + } + + [Fact] + public void FilterByEnumKey_WithEnumAsKey_DoNotThrowException() + { + // Arrange + string expectedUri = $"{ServiceRoot}/Employees(Microsoft.OData.Client.Tests.Tracking.EmployeeType'FullTime')"; + + string response = @"{ + ""@odata.context"": ""http://localhost:8007/$metadata#Employees"", + ""value"": [ + { + ""EmpNumber"": 9, + ""EmpType"": ""FullTime"", + ""OrgId"": 1, + ""Name"": ""John Doe"" + } + ] +}"; + SetupContextWithRequestPipeline(_defaultContext, response, "Employees"); + _defaultContext.SendingRequest2 += (sender, args) => + { + Assert.Equal(expectedUri, args.RequestMessage.Url.ToString()); + }; + + // Act + EmployeeSingle query = _defaultContext.Employees.ByKey( + new Dictionary() { { "EmpType", EmployeeType.FullTime } }); + + Employee employee = query.GetValue(); + + // Assert + Assert.Equal(expectedUri, query.Query.ToString()); + Assert.Equal(9, employee.EmpNumber); + Assert.Equal(EmployeeType.FullTime, employee.EmpType); + } + + private void SetupContextWithRequestPipeline(DataServiceContext context, string response, string path) + { + string location = $"{ServiceRoot}/{path}"; + + context.Configurations.RequestPipeline.OnMessageCreating = (args) => new CustomizedRequestMessage( + args, + response, + new Dictionary() + { + { "Content-Type", "application/json;charset=utf-8" }, + { "Location", location }, + }); + } + + class Container : DataServiceContext + { + public Container(Uri serviceRoot) : + base(serviceRoot, ODataProtocolVersion.V4) + { + Format.LoadServiceModel = () => CsdlReader.Parse(XmlReader.Create(new StringReader(Edmx))); + Format.UseJson(); + Employees = base.CreateQuery("Employees"); + } + + public DataServiceQuery Employees { get; private set; } + } + } + + [Key("EmpNumber", "EmpType", "OrgId")] + public class Employee : BaseEntityType + { + public int EmpNumber { get; set; } + + // Enum - Employee Type Key + public EmployeeType EmpType { get; set; } + + public int OrgId { get; set; } + + public string Name { get; set; } + + [ForeignKey("OrgId")] + public virtual Organization Organization { get; set; } + } + + public class Organization + { + public int Id { get; set; } + public string Name { get; set; } + } + + public enum EmployeeType + { + None = 1, + FullTime = 2, + PartTime = 3 + } + + public partial class EmployeeSingle : DataServiceQuerySingle + { + /// + /// Initialize a new EmployeeSingle object. + /// + public EmployeeSingle(DataServiceContext context, string path) + : base(context, path) { } + } + + public static class ExtensionMethods + { + public static EmployeeSingle ByKey(this DataServiceQuery _source, IDictionary _keys) + { + return new EmployeeSingle(_source.Context, _source.GetKeyPath(Serializer.GetKeyString(_source.Context, _keys))); + } + } +} diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/UriParser/Binders/MetadataBindingUtilsTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/UriParser/Binders/MetadataBindingUtilsTests.cs index f36f25fb5d..91251d0747 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/UriParser/Binders/MetadataBindingUtilsTests.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/UriParser/Binders/MetadataBindingUtilsTests.cs @@ -9,6 +9,7 @@ using Microsoft.OData.Edm; using Xunit; using ODataErrorStrings = Microsoft.OData.Strings; +using System.Linq; namespace Microsoft.OData.Tests.UriParser.Binders { @@ -50,6 +51,199 @@ public void IfTypesCannotPromoteErrorIsThrown() Action convertMethod = () => MetadataBindingUtils.ConvertToTypeIfNeeded(node, targetType); convertMethod.Throws(ODataErrorStrings.MetadataBinder_CannotConvertToType(node.TypeReference.FullName(), targetType.FullName())); } + + [Fact] + public void IfTypePromotionNeeded_SourceIsIntegerMemberValueAndTargetIsEnum_ConstantNodeIsCreated() + { + // Arrange + int enumValue = 3; + bool success = WeekDayEmumType.TryParse(enumValue, out IEdmEnumMember expectedMember); + + SingleValueNode source = new ConstantNode(enumValue); + IEdmTypeReference targetTypeReference = new EdmEnumTypeReference(WeekDayEmumType, false); + + // Act + ConstantNode result = MetadataBindingUtils.ConvertToTypeIfNeeded(source, targetTypeReference) as ConstantNode; + + // Assert + Assert.True(success); + result.ShouldBeEnumNode(WeekDayEmumType, expectedMember.Name); + Assert.Equal(expectedMember.Name, result.Value.ToString()); + } + + [Fact] + public void IfTypePromotionNeeded_SourceIsLongMemberValueAndTargetIsEnum_ConstantNodeIsCreated() + { + // Arrange + long enumValue = 7L; + bool success = WeekDayEmumType.TryParse(enumValue, out IEdmEnumMember expectedMember); + + SingleValueNode source = new ConstantNode(enumValue); + IEdmTypeReference targetTypeReference = new EdmEnumTypeReference(WeekDayEmumType, false); + + // Act + ConstantNode result = MetadataBindingUtils.ConvertToTypeIfNeeded(source, targetTypeReference) as ConstantNode; + + // Assert + Assert.True(success); + result.ShouldBeEnumNode(WeekDayEmumType, expectedMember.Name); + Assert.Equal(expectedMember.Name, result.Value.ToString()); // Compare the enum member name + } + + [Fact] + public void IfTypePromotionNeeded_SourceIsHugeLongMemberValueAndTargetIsEnum_ConstantNodeIsCreated() + { + // Arrange + long enumValue = 2147483657; // ((long)int.MaxValue + 10L).ToString() + bool success = EmployeeType.TryParse(enumValue, out IEdmEnumMember expectedMember); + + SingleValueNode source = new ConstantNode(enumValue); + IEdmTypeReference targetTypeReference = new EdmEnumTypeReference(EmployeeType, false); + + // Act + ConstantNode result = MetadataBindingUtils.ConvertToTypeIfNeeded(source, targetTypeReference) as ConstantNode; + + // Assert + Assert.True(success); + result.ShouldBeEnumNode(EmployeeType, expectedMember.Name); + Assert.Equal(expectedMember.Name, result.Value.ToString()); // Compare the enum member name + } + + [Fact] + public void IfTypePromotionNeeded_SourceIsLongAsStringMemberValueAndTargetIsEnum_ConstantNodeIsCreated() + { + // Arrange + string enumValue = "4294967294"; // ((long)int.MaxValue + (long)int.MaxValue).ToString(); + bool success = EmployeeType.TryParse(long.Parse(enumValue), out IEdmEnumMember expectedMember); + + SingleValueNode source = new ConstantNode(enumValue); + IEdmTypeReference targetTypeReference = new EdmEnumTypeReference(EmployeeType, false); + + // Act + ConstantNode result = MetadataBindingUtils.ConvertToTypeIfNeeded(source, targetTypeReference) as ConstantNode; + + // Assert + Assert.True(success); + result.ShouldBeEnumNode(EmployeeType, expectedMember.Name); + Assert.Equal(expectedMember.Name, result.Value.ToString()); // Compare the enum member name + } + + [Fact] + public void IfTypePromotionNeededForEnum_SourceIsIntegralMemberValueInStringAndTargetIsEnumType_ConstantNodeIsCreated() + { + // Arrange + string enumValue = "5"; + bool success = WeekDayEmumType.TryParse(long.Parse(enumValue), out IEdmEnumMember expectedMember); + + SingleValueNode source = new ConstantNode(enumValue); + IEdmTypeReference targetTypeReference = new EdmEnumTypeReference(WeekDayEmumType, false); + + // Act + ConstantNode result = MetadataBindingUtils.ConvertToTypeIfNeeded(source, targetTypeReference) as ConstantNode; + + // Assert + Assert.True(success); + result.ShouldBeEnumNode(WeekDayEmumType, expectedMember.Name); + Assert.Equal(expectedMember.Name, result.Value.ToString()); // compare the enum member name + } + + [Fact] + public void IfTypePromotionNeededForEnum_SourceIsMemberName_ConstantNodeIsCreated() + { + // Arrange + string enumValue = "Monday"; + SingleValueNode source = new ConstantNode(enumValue); + IEdmTypeReference targetTypeReference = new EdmEnumTypeReference(WeekDayEmumType, false); + // Act + ConstantNode result = MetadataBindingUtils.ConvertToTypeIfNeeded(source, targetTypeReference) as ConstantNode; + + // Assert + result.ShouldBeEnumNode(WeekDayEmumType, enumValue); + Assert.Equal(enumValue, result.Value.ToString()); + } + + [Fact] + public void IfTypePromotionNeededForEnum_SourceIsIntegerExceedingDefinedIntegralLimits_ValueIsNotValidEnumConstantExceptionIsThrown() + { + // Arrange + int enumValue = 10; + SingleValueNode source = new ConstantNode(enumValue); + IEdmTypeReference targetTypeReference = new EdmEnumTypeReference(WeekDayEmumType, false); + + // Act + Action convertIfNeeded = () => MetadataBindingUtils.ConvertToTypeIfNeeded(source, targetTypeReference); + + // Assert + convertIfNeeded.Throws(ODataErrorStrings.Binder_IsNotValidEnumConstant(enumValue.ToString())); + } + + [Fact] + public void IfTypePromotionNeeded_SourceIsFloatMemberValuesAndTargetIsEnum_CannotConvertToTypeExceptionIsThrown() + { + // Arrange + float[] floatValues = new float[] { 1.0F, 3.3F, 5.0F, 6.0F }; + + foreach (float enumValue in floatValues) + { + SingleValueNode source = new ConstantNode(enumValue); + IEdmTypeReference targetTypeReference = new EdmEnumTypeReference(WeekDayEmumType, false); + + // Act + Action convertIfNeeded = () => MetadataBindingUtils.ConvertToTypeIfNeeded(source, targetTypeReference); + + // Assert + convertIfNeeded.Throws(ODataErrorStrings.MetadataBinder_CannotConvertToType(source.TypeReference.FullName(), targetTypeReference.FullName())); + } + } + + [Fact] + public void IfTypePromotionNeeded_SourceIsFloatMemberValuesInStringAndTargetIsEnum_ValueIsNotValidEnumConstantExceptionIsThrown() + { + // Arrange + string[] floatValues = new string[] { "1.0", "3.1", "5.5", "7.0" }; + + foreach (string enumValue in floatValues) + { + SingleValueNode source = new ConstantNode(enumValue); + IEdmTypeReference targetTypeReference = new EdmEnumTypeReference(WeekDayEmumType, false); + + // Act + Action convertIfNeeded = () => MetadataBindingUtils.ConvertToTypeIfNeeded(source, targetTypeReference); + + // Assert + convertIfNeeded.Throws(ODataErrorStrings.Binder_IsNotValidEnumConstant(enumValue)); + } + } + + private static EdmEnumType WeekDayEmumType + { + get + { + EdmEnumType weekDayType = new EdmEnumType("NS", "WeekDay"); + weekDayType.AddMember("Monday", new EdmEnumMemberValue(1L)); + weekDayType.AddMember("Tuesday", new EdmEnumMemberValue(2L)); + weekDayType.AddMember("Wednesday", new EdmEnumMemberValue(3L)); + weekDayType.AddMember("Thursday", new EdmEnumMemberValue(4L)); + weekDayType.AddMember("Friday", new EdmEnumMemberValue(5L)); + weekDayType.AddMember("Saturday", new EdmEnumMemberValue(6L)); + weekDayType.AddMember("Sunday", new EdmEnumMemberValue(7L)); + + return weekDayType; + } + } + + private static EdmEnumType EmployeeType + { + get + { + EdmEnumType employeeType = new EdmEnumType("NS", "EmployeeType"); + employeeType.AddMember("FullTime", new EdmEnumMemberValue((long)int.MaxValue)); + employeeType.AddMember("PartTime", new EdmEnumMemberValue((long)int.MaxValue + 10L)); + employeeType.AddMember("Contractor", new EdmEnumMemberValue((long)int.MaxValue + (long)int.MaxValue)); + + return employeeType; + } + } #endregion } } diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/UriParser/TypePromotionUtilsTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/UriParser/TypePromotionUtilsTests.cs index 95f7d8d1d5..6087e676bd 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/UriParser/TypePromotionUtilsTests.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/UriParser/TypePromotionUtilsTests.cs @@ -326,6 +326,24 @@ public void EqualsOnEnumAndStringIsSupported() Assert.True(left.IsEquivalentTo(HardCodedTestModel.GetPet2PetColorPatternProperty().Type)); Assert.True(right.IsEquivalentTo(HardCodedTestModel.GetPet2PetColorPatternProperty().Type)); } + + [Fact] + public void EqualsOnEnumAndIntegralMemberValueIsSupported() + { + // Arrange + IEdmTypeReference left = HardCodedTestModel.GetPet2PetColorPatternProperty().Type; + IEdmTypeReference right = EdmCoreModel.Instance.GetInt32(true); + SingleValueNode leftNode = new SingleValuePropertyAccessNode(new ConstantNode(null)/*parent*/, new EdmStructuralProperty(new EdmEntityType("MyNamespace", "MyEntityType"), "myPropertyName", left)); + SingleValueNode rightNode = new SingleValuePropertyAccessNode(new ConstantNode(null)/*parent*/, new EdmStructuralProperty(new EdmEntityType("MyNamespace", "MyEntityType"), "myPropertyName", right)); + + // Act + bool result = TypePromotionUtils.PromoteOperandTypes(BinaryOperatorKind.Equal, leftNode, rightNode, out left, out right, new TypeFacetsPromotionRules()); + + // Assert + Assert.True(result); + Assert.True(left.IsEquivalentTo(HardCodedTestModel.GetPet2PetColorPatternProperty().Type)); + Assert.True(right.IsEquivalentTo(HardCodedTestModel.GetPet2PetColorPatternProperty().Type)); + } #endregion #region PromoteOperandType Tests (For Unary Operators)