Skip to content
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

Fix Default ODataEnumSerializer and ODataEnumDeserializer Fails to Convert Multi-Value Flag Enum Values to Lower Camel Case #1367

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
using System.Diagnostics.Contracts;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.OData.Common;
using Microsoft.AspNetCore.OData.Edm;
using Microsoft.AspNetCore.OData.Formatter.Value;
using Microsoft.OData;
using Microsoft.OData.Edm;
using Microsoft.OData.ModelBuilder;

namespace Microsoft.AspNetCore.OData.Formatter.Deserialization;

Expand Down Expand Up @@ -83,6 +85,8 @@ public override object ReadInline(object item, IEdmTypeReference edmType, ODataD

IEdmEnumType enumType = enumTypeReference.EnumDefinition();

Type clrType = readContext.Model.GetClrType(edmType);

// Enum member supports model alias case. So, try to use the Edm member name to retrieve the Enum value.
var memberMapAnnotation = readContext.Model.GetClrEnumMemberAnnotation(enumType);
if (memberMapAnnotation != null)
Expand All @@ -98,10 +102,89 @@ public override object ReadInline(object item, IEdmTypeReference edmType, ODataD
return clrMember;
}
}
else if (enumType.IsFlags)
{
var result = ReadFlagsEnumValue(enumValue, enumType, clrType, memberMapAnnotation);
if (result != null)
{
return result;
}
}
}
}

Type clrType = readContext.Model.GetClrType(edmType);
return EnumDeserializationHelpers.ConvertEnumValue(item, clrType);
}

/// <summary>
/// Reads the value of a flags enum.
/// </summary>
/// <param name="enumValue">The OData enum value.</param>
/// <param name="enumType">The EDM enum type.</param>
/// <param name="clrType">The EDM enum CLR type.</param>
/// <param name="memberMapAnnotation">The annotation containing the mapping of CLR enum members to EDM enum members.</param>
/// <returns>The deserialized flags enum value.</returns>
private static object ReadFlagsEnumValue(ODataEnumValue enumValue, IEdmEnumType enumType, Type clrType, ClrEnumMemberAnnotation memberMapAnnotation)
{
long result = 0;
clrType = TypeHelper.GetUnderlyingTypeOrSelf(clrType);

ReadOnlySpan<char> source = enumValue.Value.AsSpan().Trim();
int start = 0;
while (start < source.Length)
{
// Find the end of the current value.
int end = start;
while (end < source.Length && source[end] != ',')
{
end++;
}

// Extract the current value.
ReadOnlySpan<char> currentValue = source[start..end].Trim();

bool parsed = Enum.TryParse(clrType, currentValue, true, out object enumMemberParsed);
if (parsed)
{
result |= Convert.ToInt64((Enum)enumMemberParsed);
}
else
{
// If the value is not a valid enum member, try to match it with the EDM enum member name.
// This is needed for model alias case.
// For example,
// - if the enum member is defined as "Friday" and the value is "fri", we need to match them.
// - if the enum member is defined as "FullTime" and the value is "Full Time", we need to match them.
// - if the enum member is defined as "PartTime" and the value is "part time", we need to match them.
foreach (IEdmEnumMember enumMember in enumType.Members)
{
// Check if the current value matches the enum member name.
parsed = currentValue.Equals(enumMember.Name.AsSpan(), StringComparison.InvariantCultureIgnoreCase);
if (parsed)
{
Enum clrEnumMember = memberMapAnnotation.GetClrEnumMember(enumMember);
if(clrEnumMember != null)
{
result |= Convert.ToInt64(clrEnumMember);
break;
}

// If the enum member is not found, the value is not valid.
parsed = false;
}
}
}

// If still not valid, return null.
if (!parsed)
{
return null;
}

// Move to the next value.
start = end + 1;
}

return result == 0 ? null : Enum.ToObject(clrType, result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
//------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Threading.Tasks;
using Microsoft.AspNetCore.OData.Common;
using Microsoft.AspNetCore.OData.Edm;
using Microsoft.AspNetCore.OData.Formatter.Value;
using Microsoft.OData;
using Microsoft.OData.Edm;
using Microsoft.OData.ModelBuilder;

namespace Microsoft.AspNetCore.OData.Formatter.Serialization;

Expand Down Expand Up @@ -102,11 +104,18 @@ public virtual ODataEnumValue CreateODataEnumValue(object graph, IEdmEnumTypeRef
var memberMapAnnotation = writeContext?.Model.GetClrEnumMemberAnnotation(enumType.EnumDefinition());
if (memberMapAnnotation != null)
{
var edmEnumMember = memberMapAnnotation.GetEdmEnumMember((Enum)graph);
Enum graphEnum = (Enum)graph;

var edmEnumMember = memberMapAnnotation.GetEdmEnumMember(graphEnum);
if (edmEnumMember != null)
{
value = edmEnumMember.Name;
}
// If the enum is a flags enum, we need to handle the case where multiple flags are set
else if (enumType.EnumDefinition().IsFlags)
{
value = GetFlagsEnumValue(graphEnum, memberMapAnnotation);
}
}

ODataEnumValue enumValue = new ODataEnumValue(value, enumType.FullName());
Expand Down Expand Up @@ -171,4 +180,37 @@ private static bool ShouldSuppressTypeNameSerialization(ODataMetadataLevel metad
return false;
}
}

/// <summary>
/// Gets the combined names of the flags set in a Flags enum value.
/// </summary>
/// <param name="graphEnum">The enum value.</param>
/// <param name="memberMapAnnotation">The annotation containing the mapping of CLR enum members to EDM enum members.</param>
/// <returns>A comma-separated string of the names of the flags that are set.</returns>
private static string GetFlagsEnumValue(Enum graphEnum, ClrEnumMemberAnnotation memberMapAnnotation)
WanjohiSammy marked this conversation as resolved.
Show resolved Hide resolved
{
List<string> flagsList = new List<string>();

// Convert the enum value to a long for bitwise operations
long graphValue = Convert.ToInt64(graphEnum);

// Iterate through all enum values
foreach (Enum flag in Enum.GetValues(graphEnum.GetType()))
{
// Convert the current flag to a long
long flagValue = Convert.ToInt64(flag);

// Using bitwise operations to check if a flag is set, which is more efficient than Enum.HasFlag
if ((graphValue & flagValue) != 0 && flagValue != 0)
{
IEdmEnumMember flagMember = memberMapAnnotation.GetEdmEnumMember(flag);
if (flagMember != null)
{
flagsList.Add(flagMember.Name);
}
}
}

return string.Join(", ", flagsList);
}
}
18 changes: 18 additions & 0 deletions src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3522,6 +3522,16 @@
<member name="M:Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataEnumDeserializer.ReadInline(System.Object,Microsoft.OData.Edm.IEdmTypeReference,Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext)">
<inheritdoc />
</member>
<member name="M:Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataEnumDeserializer.ReadFlagsEnumValue(Microsoft.OData.ODataEnumValue,Microsoft.OData.Edm.IEdmEnumType,System.Type,Microsoft.OData.ModelBuilder.ClrEnumMemberAnnotation)">
<summary>
Reads the value of a flags enum.
</summary>
<param name="enumValue">The OData enum value.</param>
<param name="enumType">The EDM enum type.</param>
<param name="clrType">The EDM enum CLR type.</param>
<param name="memberMapAnnotation">The annotation containing the mapping of CLR enum members to EDM enum members.</param>
<returns>The deserialized flags enum value.</returns>
</member>
<member name="T:Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataPrimitiveDeserializer">
<summary>
Represents an <see cref="T:Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializer"/> that can read OData primitive types.
Expand Down Expand Up @@ -4651,6 +4661,14 @@
<param name="writeContext">The serializer write context.</param>
<returns>The created <see cref="T:Microsoft.OData.ODataEnumValue"/>.</returns>
</member>
<member name="M:Microsoft.AspNetCore.OData.Formatter.Serialization.ODataEnumSerializer.GetFlagsEnumValue(System.Enum,Microsoft.OData.ModelBuilder.ClrEnumMemberAnnotation)">
<summary>
Gets the combined names of the flags set in a Flags enum value.
</summary>
<param name="graphEnum">The enum value.</param>
<param name="memberMapAnnotation">The annotation containing the mapping of CLR enum members to EDM enum members.</param>
<returns>A comma-separated string of the names of the flags that are set.</returns>
</member>
<member name="T:Microsoft.AspNetCore.OData.Formatter.Serialization.ODataErrorSerializer">
<summary>
Represents an <see cref="T:Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializer"/> to serialize <see cref="T:Microsoft.OData.ODataError"/>s.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
using Microsoft.AspNetCore.OData.Deltas;
using Microsoft.AspNetCore.OData.Formatter;
using Microsoft.AspNetCore.OData.Query;
using Microsoft.AspNetCore.OData.Routing.Attributes;
using Microsoft.AspNetCore.OData.Routing.Controllers;
using Xunit;

Expand Down Expand Up @@ -46,6 +45,7 @@ private void InitEmployees()
SkillSet=new List<Skill> { Skill.CSharp, Skill.Sql },
Gender=Gender.Female,
AccessLevel=AccessLevel.Execute,
EmployeeType = EmployeeType.FullTime | EmployeeType.PartTime,
FavoriteSports=new FavoriteSports()
{
LikeMost=Sport.Pingpong,
Expand All @@ -58,6 +58,7 @@ private void InitEmployees()
SkillSet=new List<Skill>(),
Gender=Gender.Female,
AccessLevel=AccessLevel.Read,
EmployeeType = EmployeeType.Contract,
FavoriteSports=new FavoriteSports()
{
LikeMost=Sport.Pingpong,
Expand All @@ -70,6 +71,7 @@ private void InitEmployees()
SkillSet=new List<Skill> { Skill.Web, Skill.Sql },
Gender=Gender.Female,
AccessLevel=AccessLevel.Read|AccessLevel.Write,
EmployeeType = EmployeeType.Intern | EmployeeType.FullTime | EmployeeType.PartTime,
FavoriteSports=new FavoriteSports()
{
LikeMost=Sport.Pingpong|Sport.Basketball,
Expand Down Expand Up @@ -121,6 +123,13 @@ public IActionResult GetFavoriteSportsFromEmployee(int key)
return Ok(employee.FavoriteSports);
}

[EnableQuery]
public IActionResult GetEmployeeTypeFromEmployee(int key)
{
var employee = Employees.SingleOrDefault(e => e.ID == key);
return Ok(employee.EmployeeType);
}

[HttpGet("Employees({key})/FavoriteSports/LikeMost")]
public IActionResult GetFavoriteSportLikeMost(int key)
{
Expand Down
19 changes: 19 additions & 0 deletions test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsDataModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public class Employee

public AccessLevel AccessLevel { get; set; }

public EmployeeType EmployeeType { get; set; }

public FavoriteSports FavoriteSports { get; set; }
}

Expand All @@ -36,6 +38,23 @@ public enum AccessLevel
Execute = 4
}

[Flags]
[DataContract(Name = "employeeType")]
public enum EmployeeType
{
[EnumMember(Value = "full time")]
FullTime = 1,

[EnumMember(Value = "Part Time")]
PartTime = 2,

[EnumMember(Value = "contract")]
Contract = 4,

[EnumMember(Value = "intern")]
Intern = 8
}

public enum Gender
{
Male = 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public static IEdmModel GetExplicitModel()
employee.CollectionProperty<Skill>(c => c.SkillSet);
employee.EnumProperty<Gender>(c => c.Gender);
employee.EnumProperty<AccessLevel>(c => c.AccessLevel);
employee.EnumProperty<EmployeeType>(c => c.EmployeeType);
employee.ComplexProperty<FavoriteSports>(c => c.FavoriteSports);

var skill = builder.EnumType<Skill>();
Expand All @@ -37,6 +38,12 @@ public static IEdmModel GetExplicitModel()
accessLevel.Member(AccessLevel.Read);
accessLevel.Member(AccessLevel.Write);

var employeeType = builder.EnumType<EmployeeType>();
employeeType.Member(EmployeeType.FullTime);
employeeType.Member(EmployeeType.PartTime);
employeeType.Member(EmployeeType.Contract);
employeeType.Member(EmployeeType.Intern);

var favoriteSports = builder.ComplexType<FavoriteSports>();
favoriteSports.EnumProperty<Sport>(f => f.LikeMost);
favoriteSports.CollectionProperty<Sport>(f => f.Like);
Expand Down
Loading