Skip to content

Add support for Enums as Key in OData Client and Allow $filter with integer value in squote and without squote #3018

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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