Skip to content

Commit

Permalink
Fix Default ODataEnumSerializer and ODataEnumDeserializer Fails to Co…
Browse files Browse the repository at this point in the history
…nvert Multi-Value Flag Enum Values to Lower Camel Case (#1367)

* Be able to write multi-value flag enum members in lower camel case

* Added functionality to read multi-value flags enum in lower camel case
  • Loading branch information
WanjohiSammy authored Jan 8, 2025
1 parent e4a65ae commit c62784c
Show file tree
Hide file tree
Showing 8 changed files with 469 additions and 55 deletions.
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)
{
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 @@ -4661,6 +4671,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

0 comments on commit c62784c

Please sign in to comment.