Skip to content

Commit

Permalink
Add support for Enums as Key in OData Client and Allow $filter with i…
Browse files Browse the repository at this point in the history
…nteger 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
  • Loading branch information
WanjohiSammy authored Jul 23, 2024
1 parent 710bf7e commit bd08585
Show file tree
Hide file tree
Showing 8 changed files with 739 additions and 9 deletions.
4 changes: 3 additions & 1 deletion src/Microsoft.OData.Client/Metadata/ODataTypeInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
42 changes: 42 additions & 0 deletions src/Microsoft.OData.Core/EdmExtensionMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,5 +122,47 @@ public static bool HasKey(IEdmNavigationSource currentNavigationSource, IEdmStru

return false;
}

/// <summary>
/// Parse an enum integral value to enum member.
/// </summary>
/// <param name="enumType">edm enum type</param>
/// <param name="value">input integral value.</param>
/// <param name="enumMember">parsed result.</param>
/// <returns>true if parse succeeds, false if parse fails.</returns>
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;
}

/// <summary>
/// Checks if the given member name exists in the enum type.
/// </summary>
/// <param name="enumType">The enum type.</param>
/// <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)
{
foreach (IEdmEnumMember member in enumType.Members)
{
if (string.Equals(member.Name, memberName, comparison))
{
return true;
}
}

return false;
}
}
}
17 changes: 12 additions & 5 deletions src/Microsoft.OData.Core/UriParser/Binders/MetadataBindingUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
7 changes: 4 additions & 3 deletions src/Microsoft.OData.Core/UriParser/TypePromotionUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<EmployeeType>));
}

[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<InvalidOperationException>(() => ClientTypeUtil.GetKeyPropertiesOnType(employee));

//Assert
Assert.NotNull(actualException);
Assert.Equal(expectedException.Message, actualException.Message);
}

public class Person
{
[System.ComponentModel.DataAnnotations.Key]
Expand All @@ -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; }
}

}
}
Loading

0 comments on commit bd08585

Please sign in to comment.