From ee0134998385a28eccc94406e14685deb1471dc0 Mon Sep 17 00:00:00 2001 From: Samuel Wanjohi Date: Fri, 29 Nov 2024 20:46:28 +0300 Subject: [PATCH 01/11] Be able to write multi-value flag enum members in lower camel case --- .../Serialization/ODataEnumSerializer.cs | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataEnumSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataEnumSerializer.cs index 77e949498..547f027c6 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataEnumSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataEnumSerializer.cs @@ -6,6 +6,7 @@ //------------------------------------------------------------------------------ using System; +using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Threading.Tasks; using Microsoft.AspNetCore.OData.Common; @@ -13,6 +14,7 @@ using Microsoft.AspNetCore.OData.Formatter.Value; using Microsoft.OData; using Microsoft.OData.Edm; +using Microsoft.OData.ModelBuilder; namespace Microsoft.AspNetCore.OData.Formatter.Serialization; @@ -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()); @@ -171,4 +180,37 @@ private static bool ShouldSuppressTypeNameSerialization(ODataMetadataLevel metad return false; } } + + /// + /// Gets the combined names of the flags set in a Flags enum value. + /// + /// The enum value. + /// The annotation containing the mapping of CLR enum members to EDM enum members. + /// A comma-separated string of the names of the flags that are set. + private string GetFlagsEnumValue(Enum graphEnum, ClrEnumMemberAnnotation memberMapAnnotation) + { + List flagsList = new List(); + + // 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); + } } From acfb6c05445bd6bdd1624a5c3bf289ecce0404ab Mon Sep 17 00:00:00 2001 From: Samuel Wanjohi Date: Wed, 4 Dec 2024 14:43:19 +0300 Subject: [PATCH 02/11] Added functionality to read value of a flags enum --- .../Deserialization/ODataEnumDeserializer.cs | 47 ++++++++++++++++++- .../Microsoft.AspNetCore.OData.xml | 17 +++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs index c25943ade..94bf930a1 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs @@ -13,6 +13,7 @@ using Microsoft.AspNetCore.OData.Formatter.Value; using Microsoft.OData; using Microsoft.OData.Edm; +using Microsoft.OData.ModelBuilder; namespace Microsoft.AspNetCore.OData.Formatter.Deserialization; @@ -89,7 +90,7 @@ public override object ReadInline(object item, IEdmTypeReference edmType, ODataD { if (enumValue != null) { - IEdmEnumMember enumMember = enumType.Members.FirstOrDefault(m => m.Name == enumValue.Value); + IEdmEnumMember enumMember = enumType.Members.FirstOrDefault(m => m.Name.Equals(enumValue.Value, StringComparison.InvariantCultureIgnoreCase)); if (enumMember != null) { var clrMember = memberMapAnnotation.GetClrEnumMember(enumMember); @@ -98,10 +99,54 @@ public override object ReadInline(object item, IEdmTypeReference edmType, ODataD return clrMember; } } + else if (enumType.IsFlags) + { + var result = ReadFlagsEnumValue(enumValue, enumType, memberMapAnnotation); + if (result != null) + { + return result; + } + } } } Type clrType = readContext.Model.GetClrType(edmType); return EnumDeserializationHelpers.ConvertEnumValue(item, clrType); } + + /// + /// Reads the value of a flags enum. + /// + /// The OData enum value. + /// The EDM enum type. + /// The annotation containing the mapping of CLR enum members to EDM enum members. + /// The deserialized flags enum value. + private object ReadFlagsEnumValue(ODataEnumValue enumValue, IEdmEnumType enumType, ClrEnumMemberAnnotation memberMapAnnotation) + { + long result = 0; + Type clrEnumType = null; + + // For flags enum, we need to split the value and convert it to the enum value. + string[] values = enumValue.Value.Split(','); + foreach (string value in values) + { + IEdmEnumMember enumMember = enumType.Members.FirstOrDefault(m => m.Name.Equals(value.Trim(), StringComparison.InvariantCultureIgnoreCase)); + if (enumMember == null) + { + return null; + } + + var clrEnumMember = memberMapAnnotation.GetClrEnumMember(enumMember); + if (clrEnumMember != null) + { + result |= Convert.ToInt64(clrEnumMember); + if (clrEnumType == null) + { + clrEnumType = clrEnumMember.GetType(); + } + } + } + + return result == 0 ? null : Enum.ToObject(clrEnumType, result); + } } diff --git a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml index 2ec713bde..f58eb255c 100644 --- a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml +++ b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml @@ -3522,6 +3522,15 @@ + + + Reads the value of a flags enum. + + The OData enum value. + The EDM enum type. + The annotation containing the mapping of CLR enum members to EDM enum members. + The deserialized flags enum value. + Represents an that can read OData primitive types. @@ -4651,6 +4660,14 @@ The serializer write context. The created . + + + Gets the combined names of the flags set in a Flags enum value. + + The enum value. + The annotation containing the mapping of CLR enum members to EDM enum members. + A comma-separated string of the names of the flags that are set. + Represents an to serialize s. From c82e73ced86a1c311d0068a987b0f3d402ed9f9d Mon Sep 17 00:00:00 2001 From: Samuel Wanjohi Date: Wed, 4 Dec 2024 21:06:24 +0300 Subject: [PATCH 03/11] Refactor ODataEnumDeserializer and tests --- .../Deserialization/ODataEnumDeserializer.cs | 47 +++- .../Microsoft.AspNetCore.OData.xml | 4 +- .../Enums/EnumsController.cs | 10 + .../Enums/EnumsDataModel.cs | 19 ++ .../Enums/EnumsEdmModel.cs | 7 + .../Enums/EnumsTest.cs | 266 ++++++++++++++---- .../ODataEnumDeserializerTests.cs | 73 ++++- 7 files changed, 361 insertions(+), 65 deletions(-) diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs index 94bf930a1..d82b95b08 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs @@ -9,7 +9,9 @@ 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.Extensions; using Microsoft.AspNetCore.OData.Formatter.Value; using Microsoft.OData; using Microsoft.OData.Edm; @@ -82,15 +84,24 @@ public override object ReadInline(object item, IEdmTypeReference edmType, ODataD return new EdmEnumObject(enumTypeReference, enumValue.Value); } + bool enablePropertyNameCaseInsensitive = + readContext.Request != null && + readContext.Request.ODataOptions() != null && + readContext.Request.ODataOptions().RouteOptions != null && + readContext.Request.ODataOptions().RouteOptions.EnablePropertyNameCaseInsensitive; + 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) { if (enumValue != null) { - IEdmEnumMember enumMember = enumType.Members.FirstOrDefault(m => m.Name.Equals(enumValue.Value, StringComparison.InvariantCultureIgnoreCase)); + IEdmEnumMember enumMember = GetEnumMember(enumType, enumValue.Value, enablePropertyNameCaseInsensitive); + if (enumMember != null) { var clrMember = memberMapAnnotation.GetClrEnumMember(enumMember); @@ -101,7 +112,7 @@ public override object ReadInline(object item, IEdmTypeReference edmType, ODataD } else if (enumType.IsFlags) { - var result = ReadFlagsEnumValue(enumValue, enumType, memberMapAnnotation); + var result = ReadFlagsEnumValue(enumValue, enumType, clrType, enablePropertyNameCaseInsensitive, memberMapAnnotation); if (result != null) { return result; @@ -110,7 +121,6 @@ public override object ReadInline(object item, IEdmTypeReference edmType, ODataD } } - Type clrType = readContext.Model.GetClrType(edmType); return EnumDeserializationHelpers.ConvertEnumValue(item, clrType); } @@ -119,34 +129,47 @@ public override object ReadInline(object item, IEdmTypeReference edmType, ODataD /// /// The OData enum value. /// The EDM enum type. + /// The EDM enum CLR type. + /// The value indicating whether to enable case insensitive for the property name in conventional routing. /// The annotation containing the mapping of CLR enum members to EDM enum members. /// The deserialized flags enum value. - private object ReadFlagsEnumValue(ODataEnumValue enumValue, IEdmEnumType enumType, ClrEnumMemberAnnotation memberMapAnnotation) + private object ReadFlagsEnumValue(ODataEnumValue enumValue, IEdmEnumType enumType, Type clrType, bool enablePropertyNameCaseInsensitive, ClrEnumMemberAnnotation memberMapAnnotation) { long result = 0; - Type clrEnumType = null; + Type clrEnumType = TypeHelper.GetUnderlyingTypeOrSelf(clrType); // For flags enum, we need to split the value and convert it to the enum value. string[] values = enumValue.Value.Split(','); foreach (string value in values) { - IEdmEnumMember enumMember = enumType.Members.FirstOrDefault(m => m.Name.Equals(value.Trim(), StringComparison.InvariantCultureIgnoreCase)); - if (enumMember == null) + Enum clrEnumMember = null; + IEdmEnumMember enumMember = GetEnumMember(enumType, value.Trim(), enablePropertyNameCaseInsensitive); + + if (enumMember != null) + { + clrEnumMember = memberMapAnnotation.GetClrEnumMember(enumMember); + } + else if (Enum.TryParse(clrEnumType, value, true, out var enumMemberParsed)) + { + clrEnumMember = (Enum)enumMemberParsed; + } + else { return null; } - var clrEnumMember = memberMapAnnotation.GetClrEnumMember(enumMember); if (clrEnumMember != null) { result |= Convert.ToInt64(clrEnumMember); - if (clrEnumType == null) - { - clrEnumType = clrEnumMember.GetType(); - } } } return result == 0 ? null : Enum.ToObject(clrEnumType, result); } + + private IEdmEnumMember GetEnumMember(IEdmEnumType enumType, string value, bool enablePropertyNameCaseInsensitive) + { + StringComparison comparison = enablePropertyNameCaseInsensitive ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture; + return enumType.Members.FirstOrDefault(m => m.Name.Equals(value, comparison)); + } } diff --git a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml index f58eb255c..dcd8295eb 100644 --- a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml +++ b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml @@ -3522,12 +3522,14 @@ - + Reads the value of a flags enum. The OData enum value. The EDM enum type. + The EDM enum CLR type. + The value indicating whether to enable case insensitive for the property name in conventional routing. The annotation containing the mapping of CLR enum members to EDM enum members. The deserialized flags enum value. diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsController.cs index abc4f6247..5085324d1 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsController.cs @@ -46,6 +46,7 @@ private void InitEmployees() SkillSet=new List { Skill.CSharp, Skill.Sql }, Gender=Gender.Female, AccessLevel=AccessLevel.Execute, + EmployeeType = EmployeeType.FullTime | EmployeeType.PartTime, FavoriteSports=new FavoriteSports() { LikeMost=Sport.Pingpong, @@ -58,6 +59,7 @@ private void InitEmployees() SkillSet=new List(), Gender=Gender.Female, AccessLevel=AccessLevel.Read, + EmployeeType = EmployeeType.Contract, FavoriteSports=new FavoriteSports() { LikeMost=Sport.Pingpong, @@ -70,6 +72,7 @@ private void InitEmployees() SkillSet=new List { 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, @@ -121,6 +124,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) { diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsDataModel.cs index 795999463..e9d6550f7 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsDataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsDataModel.cs @@ -23,6 +23,8 @@ public class Employee public AccessLevel AccessLevel { get; set; } + public EmployeeType EmployeeType { get; set; } + public FavoriteSports FavoriteSports { get; set; } } @@ -36,6 +38,23 @@ public enum AccessLevel Execute = 4 } +[Flags] +[DataContract(Name = "employeeType")] +public enum EmployeeType +{ + [EnumMember(Value = "fulltime")] + FullTime = 1, + + [EnumMember(Value = "parttime")] + PartTime = 2, + + [EnumMember(Value = "contract")] + Contract = 4, + + [EnumMember(Value = "intern")] + Intern = 8 +} + public enum Gender { Male = 1, diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsEdmModel.cs index 3b9fe2b24..7486ce5c2 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsEdmModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsEdmModel.cs @@ -21,6 +21,7 @@ public static IEdmModel GetExplicitModel() employee.CollectionProperty(c => c.SkillSet); employee.EnumProperty(c => c.Gender); employee.EnumProperty(c => c.AccessLevel); + employee.EnumProperty(c => c.EmployeeType); employee.ComplexProperty(c => c.FavoriteSports); var skill = builder.EnumType(); @@ -37,6 +38,12 @@ public static IEdmModel GetExplicitModel() accessLevel.Member(AccessLevel.Read); accessLevel.Member(AccessLevel.Write); + var employeeType = builder.EnumType(); + employeeType.Member(EmployeeType.FullTime); + employeeType.Member(EmployeeType.PartTime); + employeeType.Member(EmployeeType.Contract); + employeeType.Member(EmployeeType.Intern); + var favoriteSports = builder.ComplexType(); favoriteSports.EnumProperty(f => f.LikeMost); favoriteSports.CollectionProperty(f => f.Like); diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsTest.cs index 9fa8baf87..81771356e 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsTest.cs @@ -12,10 +12,10 @@ using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; -using Microsoft.AspNetCore.OData.TestCommon; using Microsoft.AspNetCore.OData.E2E.Tests.Commons; using Microsoft.AspNetCore.OData.E2E.Tests.Extensions; using Microsoft.AspNetCore.OData.Routing.Controllers; +using Microsoft.AspNetCore.OData.TestCommon; using Microsoft.Extensions.DependencyInjection; using Microsoft.OData; using Microsoft.OData.Edm; @@ -35,7 +35,7 @@ protected static void UpdateConfigureServices(IServiceCollection services) IEdmModel model2 = EnumsEdmModel.GetExplicitModel(); services.AddControllers().AddOData(opt => opt.Count().Filter().Expand().Select().OrderBy().SetMaxTop(5) - .AddRouteComponents("convention", model1) + .AddRouteComponents("convention", model1) .AddRouteComponents("explicit", model2)); } @@ -82,7 +82,7 @@ public async Task ModelBuilderTest(string modelMode) var employee = edmModel.SchemaElements.SingleOrDefault(e => e.Name == "Employee") as IEdmEntityType; Assert.Single(employee.Key()); Assert.Equal("ID", employee.Key().First().Name); - Assert.Equal(6, employee.Properties().Count()); + Assert.Equal(7, employee.Properties().Count()); //Entity Enum Collection Property var skillSet = employee.Properties().SingleOrDefault(p => p.Name == "SkillSet"); @@ -99,6 +99,11 @@ public async Task ModelBuilderTest(string modelMode) Assert.Equal(3, edmEnumType.Members.Count()); Assert.True(edmEnumType.IsFlags); + var employeeType = employee.Properties().SingleOrDefault(p => p.Name == "EmployeeType") as IEdmStructuralProperty; + edmEnumType = employeeType.Type.Definition as IEdmEnumType; + Assert.Equal(4, edmEnumType.Members.Count()); + Assert.True(edmEnumType.IsFlags); + //Action AddSkill var iEdmOperation = edmModel.FindOperations(typeof(Employee).Namespace + ".AddSkill").FirstOrDefault(); var iEdmOperationParameter = iEdmOperation.Parameters.SingleOrDefault(p => p.Name == "skill"); @@ -257,6 +262,69 @@ public async Task QueryEnumPropertyInEntityType(string format) var context = json["@odata.context"].ToString(); Assert.True(context.IndexOf("/$metadata#Collection(Microsoft.AspNetCore.OData.E2E.Tests.Enums.Skill)") >= 0); } + + requestUri = "/convention/Employees(1)/EmployeeType?$format=" + format; + response = await client.GetAsync(requestUri); + json = await response.Content.ReadAsObject(); + var employeeTypeValue = json.GetValue("value").ToString(); + Assert.Equal("fulltime, parttime", employeeTypeValue); + if (format != "application/json;odata.metadata=none") + { + var context = json.GetValue("@odata.context").ToString(); + Assert.True(context.IndexOf("/$metadata#Employees(1)/EmployeeType") > 0); + } + } + + public static TheoryDataSet QueryEnumPropertyWithFlagsInEntityTypeData + { + get + { + return new TheoryDataSet + { + { "application/json;odata.metadata=full", 1, "Execute", "fulltime, parttime" }, + { "application/json;odata.metadata=minimal", 1, "Execute", "fulltime, parttime" }, + { "application/json;odata.metadata=none", 1, "Execute", "fulltime, parttime" }, + + { "application/json;odata.metadata=full", 2, "Read", "contract" }, + { "application/json;odata.metadata=minimal", 2, "Read", "contract" }, + { "application/json;odata.metadata=none", 2, "Read", "contract" }, + + { "application/json;odata.metadata=full", 3, "Read, Write", "intern, fulltime, parttime" }, + { "application/json;odata.metadata=minimal", 3, "Read, Write", "intern, fulltime, parttime" }, + { "application/json;odata.metadata=none", 3, "Read, Write", "intern, fulltime, parttime" }, + }; + } + } + + [Theory] + [MemberData(nameof(QueryEnumPropertyWithFlagsInEntityTypeData))] + public async Task QueryEnumPropertyWithFlagsInEntityType(string format, int id, string expectedAccessLevelValue,string expectedEmployeeTypeValue) + { + // Arrange + await ResetDatasource(); + HttpClient client = CreateClient(); + + string requestUri = $"/convention/Employees({id})?$format={format}"; + + // Act + var response = await client.GetAsync(requestUri); + + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsObject(); + + var accessLevel = json.GetValue("AccessLevel").ToString(); + var employeeType = json.GetValue("EmployeeType").ToString(); + + // Assert + Assert.Equal(expectedAccessLevelValue, accessLevel); + Assert.Equal(expectedAccessLevelValue, accessLevel); + + if (format != "application/json;odata.metadata=none") + { + var context = json.GetValue("@odata.context").ToString(); + Assert.True(context.IndexOf("/$metadata#Employees/$entity") > 0); + } } [Theory] @@ -417,48 +485,104 @@ public async Task EnumInOrderBy(string format) Assert.Equal(3, secondEmployee["ID"]); } -#region Update - - //[Fact] - //public async Task AddEntity() - //{ - // await ResetDatasource(); - // string requestUri = "/convention/Employees?$format=application/json;odata.metadata=none"; - - // using (HttpResponseMessage response = await this.Client.GetAsync(requestUri)) - // { - // response.EnsureSuccessStatusCode(); - - // var json = await response.Content.ReadAsObject(); - // var result = json.GetValue("value") as JArray; - // Assert.Equal(3, result.Count); - // } - - // var postUri = "/convention/Employees"; - - // var postContent = JObject.Parse(@"{""ID"":1, - // ""Name"":""Name2"", - // ""SkillSet"":[""Sql""], - // ""Gender"":""Female"", - // ""AccessLevel"":""Read,Write"", - // ""FavoriteSports"":{ - // ""LikeMost"":""Pingpong"", - // ""Like"":[""Pingpong"",""Basketball""] - // }}"); - // using (HttpResponseMessage response = await this.Client.PostAsJsonAsync(postUri, postContent)) - // { - // Assert.Equal(HttpStatusCode.Created, response.StatusCode); - // } - - // using (HttpResponseMessage response = await this.Client.GetAsync(requestUri)) - // { - // response.EnsureSuccessStatusCode(); - - // var json = await response.Content.ReadAsObject(); - // var result = json.GetValue("value") as JArray; - // Assert.Equal(4, result.Count); - // } - //} + #region Add + + [Fact] + public async Task AddEntity() + { + await ResetDatasource(); + HttpClient client = CreateClient(); + string requestUri = "/convention/Employees?$format=application/json;odata.metadata=none"; + + using (HttpResponseMessage response = await client.GetAsync(requestUri)) + { + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsObject(); + var result = json.GetValue("value") as JArray; + Assert.Equal(3, result.Count); + } + + var postUri = "/convention/Employees"; + + var postContent = JObject.Parse(@"{""ID"":6, + ""Name"":""New Name 20"", + ""SkillSet"":[""Sql""], + ""Gender"":""Female"", + ""AccessLevel"":""read,write"", + ""EmployeeType"":""intern, contract"", + ""FavoriteSports"":{ + ""LikeMost"":""Pingpong"", + ""Like"":[""Pingpong"",""Basketball""] + }}"); + using (HttpResponseMessage response = await client.PostAsJsonAsync(postUri, postContent)) + { + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + } + + using (HttpResponseMessage response = await client.GetAsync(requestUri)) + { + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsObject(); + var result = json.GetValue("value") as JArray; + Assert.Equal(4, result.Count); + } + } + + [Theory] + [InlineData("read", "intern")] + [InlineData("Read", "Intern")] + [InlineData("read, Execute", "fullTime, Contract")] + [InlineData("Read, Execute", "Intern, FullTime")] + [InlineData("read,write", "intern, contract")] + public async Task AddEntityWithLowerAndUpperCamelCaseFlagEnums(string accessLevelValue, string employeeTypeValue) + { + await ResetDatasource(); + HttpClient client = CreateClient(); + string requestUri = "/convention/Employees?$format=application/json;odata.metadata=none"; + + using (HttpResponseMessage response = await client.GetAsync(requestUri)) + { + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsObject(); + var result = json.GetValue("value") as JArray; + Assert.Equal(3, result.Count); + } + + var postUri = "/convention/Employees"; + + var postContent = JObject.Parse($@"{{ + ""ID"":6, + ""Name"":""New Name 23"", + ""SkillSet"":[""Sql""], + ""Gender"":""Female"", + ""AccessLevel"":""{accessLevelValue}"", + ""EmployeeType"":""{employeeTypeValue}"", + ""FavoriteSports"":{{ + ""LikeMost"":""Pingpong"", + ""Like"":[""Pingpong"",""Basketball""] + }}}}"); + + using (HttpResponseMessage response = await client.PostAsJsonAsync(postUri, postContent)) + { + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + } + + using (HttpResponseMessage response = await client.GetAsync(requestUri)) + { + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsObject(); + var result = json.GetValue("value") as JArray; + Assert.Equal(4, result.Count); + } + } + + #endregion + + #region Update [Fact] public async Task PostToEnumCollection() @@ -536,6 +660,7 @@ public async Task UpdateEntity() ""SkillSet"":[""Sql""], ""Gender"":""Female"", ""AccessLevel"":""Execute,Write"", + ""EmployeeType"":""intern, contract"", ""FavoriteSports"":{ ""LikeMost"":""Basketball"", ""Like"":[""Pingpong"",""Basketball""] @@ -554,6 +679,9 @@ public async Task UpdateEntity() var accessLevel = json.GetValue("AccessLevel"); Assert.Equal("Write, Execute", accessLevel); + var employeeType = json.GetValue("EmployeeType"); + Assert.Equal("contract, intern", employeeType); + var skillSet = json.GetValue("SkillSet").ToString(); Assert.Equal(@"[""Sql""]", skillSet.Replace("\r\n", "").Replace(" ", "")); @@ -579,6 +707,7 @@ public async Task UpsertEntity(string method) ""SkillSet"":[""Sql""], ""Gender"":""Female"", ""AccessLevel"":""Execute,Write"", + ""EmployeeType"":""parttime, Fulltime"", ""FavoriteSports"":{ ""LikeMost"":""Basketball"", ""Like"":[""Pingpong"",""Basketball""] @@ -598,9 +727,52 @@ public async Task UpsertEntity(string method) } } -#endregion + public static TheoryDataSet UpsertEntityWithLowerAndUpperCamelCaseFlagEnumsData + { + get + { + return new TheoryDataSet + { + { "PUT", @"{""ID"":20,""AccessLevel"":""Execute,Write"",""EmployeeType"":""intern, FullTime""}" }, + { "PATCH", @"{""ID"":20,""AccessLevel"":""Execute,Write"",""EmployeeType"":""intern, FullTime""}" }, + { "PUT", @"{""ID"":20,""AccessLevel"":""Execute, write"",""EmployeeType"":""Intern, contract""}" }, + { "PATCH", @"{""ID"":20,""AccessLevel"":""Execute, write"",""EmployeeType"":""Intern, contract""}" }, + { "PUT", @"{""ID"":20,""AccessLevel"":""execute"",""EmployeeType"":""contract""}" }, + { "PATCH", @"{""ID"":20,""AccessLevel"":""execute"",""EmployeeType"":""contract""}" }, + { "PUT", @"{""ID"":20,""AccessLevel"":""Execute"",""EmployeeType"":""FullTime""}" }, + { "PATCH", @"{""ID"":20,""AccessLevel"":""Execute"",""EmployeeType"":""FullTime""}" }, + }; + } + } + + + [Theory] + [MemberData(nameof(UpsertEntityWithLowerAndUpperCamelCaseFlagEnumsData))] + public async Task UpsertEntityWithLowerAndUpperCamelCaseFlagEnums(string method, string requestContent) + { + await ResetDatasource(); + HttpClient client = CreateClient(); + + var requestUri = "/convention/Employees(20)"; + + HttpRequestMessage request = new HttpRequestMessage(new HttpMethod(method), requestUri); + request.Content = new StringContent(requestContent); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = requestContent.Length; + request.Headers.Add("Prefer", "return=minimal"); + using (HttpResponseMessage response = await client.SendAsync(request)) + { + Assert.True(HttpStatusCode.NoContent == response.StatusCode, + string.Format("Response code is not right, expected: {0}, actual: {1}", HttpStatusCode.NoContent, response.StatusCode)); + Assert.True(response.Headers.Contains("OData-EntityId"), "The response should contain Header 'OData-EntityId'"); + Assert.True(response.Headers.Contains("Location"), "The response should contain Header 'Location'"); + Assert.True(response.Headers.Contains("OData-Version"), "The response should contain Header 'OData-Version'"); + } + } + + #endregion -#region Delete + #region Delete [Fact] public async Task DeleteEntity() diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataEnumDeserializerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataEnumDeserializerTests.cs index 4f665f7b4..8b3083311 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataEnumDeserializerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataEnumDeserializerTests.cs @@ -5,6 +5,7 @@ // //------------------------------------------------------------------------------ +using System; using System.Runtime.Serialization; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -114,12 +115,14 @@ public async Task ReadAsync_Works_ForUnType() Assert.Equal("Blue", color.Value); } - [Fact] - public async Task ReadAsync_Works_ForModelAlias() + [Theory] + [InlineData("{\"@odata.type\":\"#NS.level\",\"value\":\"veryhigh\"}", Level.High)] + [InlineData("{\"@odata.type\":\"#NS.level\",\"value\":\"High\"}", Level.High)] + [InlineData("{\"@odata.type\":\"#NS.level\",\"value\":\"low\"}", Level.Low)] + [InlineData("{\"@odata.type\":\"#NS.level\",\"value\":\"Low\"}", Level.Low)] + public async Task ReadAsync_Works_ForModelAlias(string content, Level expectedLevel) { // Arrange - string content = "{\"@odata.type\":\"#NS.level\",\"value\":\"veryhigh\"}"; - var builder = new ODataConventionModelBuilder(); builder.EnumType().Namespace = "NS"; IEdmModel model = builder.GetEdmModel(); @@ -139,7 +142,47 @@ public async Task ReadAsync_Works_ForModelAlias() // Assert Level level = Assert.IsType(value); - Assert.Equal(Level.High, level); + Assert.Equal(expectedLevel, level); + } + + [Theory] + [InlineData("{\"@odata.type\":\"#NS.day\",\"value\":\"monday\"}", Day.Monday)] + [InlineData("{\"@odata.type\":\"#NS.day\",\"value\":\"Monday\"}", Day.Monday)] + [InlineData("{\"@odata.type\":\"#NS.day\",\"value\":\"monday, tuesday\"}", Day.Monday | Day.Tuesday)] + [InlineData("{\"@odata.type\":\"#NS.day\",\"value\":\"Monday, tuesday\"}", Day.Monday | Day.Tuesday)] + [InlineData("{\"@odata.type\":\"#NS.day\",\"value\":\"Monday, Tuesday\"}", Day.Monday | Day.Tuesday)] + [InlineData("{\"@odata.type\":\"#NS.day\",\"value\":\"monday, tuesday, thursday\"}", Day.Monday | Day.Tuesday | Day.Thursday)] + [InlineData("{\"@odata.type\":\"#NS.day\",\"value\":\"Monday, Tuesday, thursday\"}", Day.Monday | Day.Tuesday | Day.Thursday)] + [InlineData("{\"@odata.type\":\"#NS.day\",\"value\":\"Monday, tuesday, Thursday\"}", Day.Monday | Day.Tuesday | Day.Thursday)] + [InlineData("{\"@odata.type\":\"#NS.day\",\"value\":\"Monday, Tuesday, Thursday\"}", Day.Monday | Day.Tuesday | Day.Thursday)] + [InlineData("{\"@odata.type\":\"#NS.day\",\"value\":\"Monday, Friday\"}", Day.Monday | Day.Friday)] + [InlineData("{\"@odata.type\":\"#NS.day\",\"value\":\"Monday, tuesday, fri\"}", Day.Monday | Day.Tuesday | Day.Friday)] + [InlineData("{\"@odata.type\":\"#NS.day\",\"value\":\"Monday, fri, Wednesday\"}", Day.Monday | Day.Friday | Day.Wednesday)] + [InlineData("{\"@odata.type\":\"#NS.day\",\"value\":\"Monday, fri\"}", Day.Monday | Day.Friday)] + [InlineData("{\"@odata.type\":\"#NS.day\",\"value\":\"monday, fri\"}", Day.Monday | Day.Friday)] + public async Task ReadAsync_Works_ForModelAliasWithFlags(string content, Day expectedDay) + { + // Arrange + var builder = new ODataConventionModelBuilder(); + builder.EnumType().Namespace = "NS"; + IEdmModel model = builder.GetEdmModel(); + + ODataEnumDeserializer deserializer = new ODataEnumDeserializer(); + ODataDeserializerContext readContext = new ODataDeserializerContext + { + Model = model, + ResourceType = typeof(Day) + }; + + HttpRequest request = RequestFactory.Create("Post", "http://localhost/", _edmModel); + + // Act + object value = await deserializer.ReadAsync(ODataTestUtil.GetODataMessageReader(request.GetODataMessage(content), model), + typeof(Day), readContext); + + // Assert + Day day = Assert.IsType(value); + Assert.Equal(expectedDay, day); } [Fact] @@ -176,4 +219,24 @@ public enum Level [EnumMember(Value = "veryhigh")] High } + + [Flags] + [DataContract(Name = "day")] + public enum Day + { + [EnumMember(Value = "monday")] + Monday = 1, + + [EnumMember(Value = "tuesday")] + Tuesday = 2, + + [EnumMember(Value = "wednesday")] + Wednesday = 4, + + [EnumMember(Value = "thursday")] + Thursday = 8, + + [EnumMember(Value = "fri")] + Friday = 16 + } } From 1343e4da7959a96fae5f8f38da870d7c936a5eaf Mon Sep 17 00:00:00 2001 From: Samuel Wanjohi Date: Thu, 5 Dec 2024 12:10:25 +0300 Subject: [PATCH 04/11] Refactor to use ReadOnlySpan other than string[] --- .../Deserialization/ODataEnumDeserializer.cs | 50 ++++++++----------- .../Microsoft.AspNetCore.OData.xml | 3 +- 2 files changed, 23 insertions(+), 30 deletions(-) diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs index d82b95b08..f8e060504 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs @@ -11,7 +11,6 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.OData.Common; using Microsoft.AspNetCore.OData.Edm; -using Microsoft.AspNetCore.OData.Extensions; using Microsoft.AspNetCore.OData.Formatter.Value; using Microsoft.OData; using Microsoft.OData.Edm; @@ -84,12 +83,6 @@ public override object ReadInline(object item, IEdmTypeReference edmType, ODataD return new EdmEnumObject(enumTypeReference, enumValue.Value); } - bool enablePropertyNameCaseInsensitive = - readContext.Request != null && - readContext.Request.ODataOptions() != null && - readContext.Request.ODataOptions().RouteOptions != null && - readContext.Request.ODataOptions().RouteOptions.EnablePropertyNameCaseInsensitive; - IEdmEnumType enumType = enumTypeReference.EnumDefinition(); Type clrType = readContext.Model.GetClrType(edmType); @@ -100,8 +93,7 @@ public override object ReadInline(object item, IEdmTypeReference edmType, ODataD { if (enumValue != null) { - IEdmEnumMember enumMember = GetEnumMember(enumType, enumValue.Value, enablePropertyNameCaseInsensitive); - + IEdmEnumMember enumMember = enumType.Members.FirstOrDefault(m => m.Name.Equals(enumValue.Value, StringComparison.InvariantCultureIgnoreCase)); if (enumMember != null) { var clrMember = memberMapAnnotation.GetClrEnumMember(enumMember); @@ -112,7 +104,7 @@ public override object ReadInline(object item, IEdmTypeReference edmType, ODataD } else if (enumType.IsFlags) { - var result = ReadFlagsEnumValue(enumValue, enumType, clrType, enablePropertyNameCaseInsensitive, memberMapAnnotation); + var result = ReadFlagsEnumValue(enumValue, enumType, clrType, memberMapAnnotation); if (result != null) { return result; @@ -130,46 +122,48 @@ public override object ReadInline(object item, IEdmTypeReference edmType, ODataD /// The OData enum value. /// The EDM enum type. /// The EDM enum CLR type. - /// The value indicating whether to enable case insensitive for the property name in conventional routing. /// The annotation containing the mapping of CLR enum members to EDM enum members. /// The deserialized flags enum value. - private object ReadFlagsEnumValue(ODataEnumValue enumValue, IEdmEnumType enumType, Type clrType, bool enablePropertyNameCaseInsensitive, ClrEnumMemberAnnotation memberMapAnnotation) + private static object ReadFlagsEnumValue(ODataEnumValue enumValue, IEdmEnumType enumType, Type clrType, ClrEnumMemberAnnotation memberMapAnnotation) { long result = 0; Type clrEnumType = TypeHelper.GetUnderlyingTypeOrSelf(clrType); + // use `stackalloc` to allocate a Span on the stack, which avoids heap allocations. + Span ranges = stackalloc Range[enumValue.Value.Count(c => c == ',') + 1]; + ReadOnlySpan source = enumValue.Value.AsSpan(); + // For flags enum, we need to split the value and convert it to the enum value. - string[] values = enumValue.Value.Split(','); - foreach (string value in values) + int count = source.Split(ranges, ',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + for(int i = 0; i < count; i++) { - Enum clrEnumMember = null; - IEdmEnumMember enumMember = GetEnumMember(enumType, value.Trim(), enablePropertyNameCaseInsensitive); + ReadOnlySpan value = source[ranges[i]]; + IEdmEnumMember enumMember = null; + foreach (IEdmEnumMember member in enumType.Members) + { + if (value.Equals(member.Name, StringComparison.InvariantCultureIgnoreCase)) + { + enumMember = member; + break; + } + } if (enumMember != null) { - clrEnumMember = memberMapAnnotation.GetClrEnumMember(enumMember); + Enum clrEnumMember = memberMapAnnotation.GetClrEnumMember(enumMember); + result |= Convert.ToInt64(clrEnumMember); } else if (Enum.TryParse(clrEnumType, value, true, out var enumMemberParsed)) { - clrEnumMember = (Enum)enumMemberParsed; + result |= Convert.ToInt64((Enum)enumMemberParsed); } else { return null; } - - if (clrEnumMember != null) - { - result |= Convert.ToInt64(clrEnumMember); - } } return result == 0 ? null : Enum.ToObject(clrEnumType, result); } - private IEdmEnumMember GetEnumMember(IEdmEnumType enumType, string value, bool enablePropertyNameCaseInsensitive) - { - StringComparison comparison = enablePropertyNameCaseInsensitive ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture; - return enumType.Members.FirstOrDefault(m => m.Name.Equals(value, comparison)); - } } diff --git a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml index dcd8295eb..dd2c60d0e 100644 --- a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml +++ b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml @@ -3522,14 +3522,13 @@ - + Reads the value of a flags enum. The OData enum value. The EDM enum type. The EDM enum CLR type. - The value indicating whether to enable case insensitive for the property name in conventional routing. The annotation containing the mapping of CLR enum members to EDM enum members. The deserialized flags enum value. From 0f9fd310f8acb1e3ed9697b4df8b160ce7d01531 Mon Sep 17 00:00:00 2001 From: Samuel Wanjohi Date: Thu, 5 Dec 2024 12:59:18 +0300 Subject: [PATCH 05/11] Remove duplicate assertion --- .../Enums/EnumsTest.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsTest.cs index 81771356e..dd39d94ab 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsTest.cs @@ -289,9 +289,9 @@ public static TheoryDataSet QueryEnumPropertyWithFl { "application/json;odata.metadata=minimal", 2, "Read", "contract" }, { "application/json;odata.metadata=none", 2, "Read", "contract" }, - { "application/json;odata.metadata=full", 3, "Read, Write", "intern, fulltime, parttime" }, - { "application/json;odata.metadata=minimal", 3, "Read, Write", "intern, fulltime, parttime" }, - { "application/json;odata.metadata=none", 3, "Read, Write", "intern, fulltime, parttime" }, + { "application/json;odata.metadata=full", 3, "Read, Write", "fulltime, parttime, intern" }, + { "application/json;odata.metadata=minimal", 3, "Read, Write", "fulltime, parttime, intern" }, + { "application/json;odata.metadata=none", 3, "Read, Write", "fulltime, parttime, intern" }, }; } } @@ -318,7 +318,7 @@ public async Task QueryEnumPropertyWithFlagsInEntityType(string format, int id, // Assert Assert.Equal(expectedAccessLevelValue, accessLevel); - Assert.Equal(expectedAccessLevelValue, accessLevel); + Assert.Equal(expectedEmployeeTypeValue, employeeType); if (format != "application/json;odata.metadata=none") { From d4c84d77d0d90271b2420af9429b8d5df1fbd262 Mon Sep 17 00:00:00 2001 From: Samuel Wanjohi Date: Mon, 9 Dec 2024 20:39:45 +0300 Subject: [PATCH 06/11] Refactor --- .../Formatter/Deserialization/ODataEnumDeserializer.cs | 3 ++- .../Formatter/Serialization/ODataEnumSerializer.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs index f8e060504..2caeb29a5 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs @@ -138,6 +138,8 @@ private static object ReadFlagsEnumValue(ODataEnumValue enumValue, IEdmEnumType for(int i = 0; i < count; i++) { ReadOnlySpan value = source[ranges[i]]; + + // Try to find the enum member in the EDM model. IEdmEnumMember enumMember = null; foreach (IEdmEnumMember member in enumType.Members) { @@ -165,5 +167,4 @@ private static object ReadFlagsEnumValue(ODataEnumValue enumValue, IEdmEnumType return result == 0 ? null : Enum.ToObject(clrEnumType, result); } - } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataEnumSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataEnumSerializer.cs index 547f027c6..c90a5d92f 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataEnumSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataEnumSerializer.cs @@ -187,7 +187,7 @@ private static bool ShouldSuppressTypeNameSerialization(ODataMetadataLevel metad /// The enum value. /// The annotation containing the mapping of CLR enum members to EDM enum members. /// A comma-separated string of the names of the flags that are set. - private string GetFlagsEnumValue(Enum graphEnum, ClrEnumMemberAnnotation memberMapAnnotation) + private static string GetFlagsEnumValue(Enum graphEnum, ClrEnumMemberAnnotation memberMapAnnotation) { List flagsList = new List(); From 261d0fcf71b6ac99464be0113f8d70f12df54dbd Mon Sep 17 00:00:00 2001 From: Samuel Wanjohi Date: Fri, 13 Dec 2024 13:03:10 +0300 Subject: [PATCH 07/11] Refactor split logic --- .../Deserialization/ODataEnumDeserializer.cs | 55 +++++++++++-------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs index 2caeb29a5..f08e3bfe0 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs @@ -6,6 +6,7 @@ //------------------------------------------------------------------------------ using System; +using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Linq; using System.Threading.Tasks; @@ -128,41 +129,49 @@ private static object ReadFlagsEnumValue(ODataEnumValue enumValue, IEdmEnumType { long result = 0; Type clrEnumType = TypeHelper.GetUnderlyingTypeOrSelf(clrType); + ReadOnlySpan source = enumValue.Value.AsSpan().Trim(); - // use `stackalloc` to allocate a Span on the stack, which avoids heap allocations. - Span ranges = stackalloc Range[enumValue.Value.Count(c => c == ',') + 1]; - ReadOnlySpan source = enumValue.Value.AsSpan(); - - // For flags enum, we need to split the value and convert it to the enum value. - int count = source.Split(ranges, ',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - for(int i = 0; i < count; i++) + int start = 0; + while (start < source.Length) { - ReadOnlySpan value = source[ranges[i]]; - - // Try to find the enum member in the EDM model. - IEdmEnumMember enumMember = null; - foreach (IEdmEnumMember member in enumType.Members) + // Find the end of the current value. + int end = start; + while (end < source.Length && source[end] != ',') { - if (value.Equals(member.Name, StringComparison.InvariantCultureIgnoreCase)) - { - enumMember = member; - break; - } + end++; } - if (enumMember != null) - { - Enum clrEnumMember = memberMapAnnotation.GetClrEnumMember(enumMember); - result |= Convert.ToInt64(clrEnumMember); - } - else if (Enum.TryParse(clrEnumType, value, true, out var enumMemberParsed)) + // Extract the current value. + ReadOnlySpan currentValue = source[start..end].Trim(); + + bool parsed = Enum.TryParse(clrEnumType, currentValue, true, out var enumMemberParsed); + if (parsed) { result |= Convert.ToInt64((Enum)enumMemberParsed); } else + { + 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); + result |= Convert.ToInt64(clrEnumMember); + break; + } + } + } + + // If still not valid, return null. + if (!parsed) { return null; } + + // Move to the next value. + start = end + 1; } return result == 0 ? null : Enum.ToObject(clrEnumType, result); From 301208a33d71b2f1f1b3728b6f52244c7dbe188a Mon Sep 17 00:00:00 2001 From: Samuel Wanjohi Date: Fri, 13 Dec 2024 14:21:11 +0300 Subject: [PATCH 08/11] Added more tests --- .../Deserialization/ODataEnumDeserializer.cs | 9 ++++++++- .../Enums/EnumsDataModel.cs | 4 ++-- .../Enums/EnumsTest.cs | 14 +++++++------- .../Deserialization/ODataEnumDeserializerTests.cs | 11 ++++++----- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs index f08e3bfe0..6dbd4970f 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs @@ -144,13 +144,20 @@ private static object ReadFlagsEnumValue(ODataEnumValue enumValue, IEdmEnumType // Extract the current value. ReadOnlySpan currentValue = source[start..end].Trim(); - bool parsed = Enum.TryParse(clrEnumType, currentValue, true, out var enumMemberParsed); + bool parsed = Enum.TryParse(clrEnumType, 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. + parsed = false; foreach (IEdmEnumMember enumMember in enumType.Members) { // Check if the current value matches the enum member name. diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsDataModel.cs index e9d6550f7..08eea53c5 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsDataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsDataModel.cs @@ -42,10 +42,10 @@ public enum AccessLevel [DataContract(Name = "employeeType")] public enum EmployeeType { - [EnumMember(Value = "fulltime")] + [EnumMember(Value = "full time")] FullTime = 1, - [EnumMember(Value = "parttime")] + [EnumMember(Value = "Part Time")] PartTime = 2, [EnumMember(Value = "contract")] diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsTest.cs index dd39d94ab..82e499a1c 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsTest.cs @@ -267,7 +267,7 @@ public async Task QueryEnumPropertyInEntityType(string format) response = await client.GetAsync(requestUri); json = await response.Content.ReadAsObject(); var employeeTypeValue = json.GetValue("value").ToString(); - Assert.Equal("fulltime, parttime", employeeTypeValue); + Assert.Equal("full time, Part Time", employeeTypeValue); if (format != "application/json;odata.metadata=none") { var context = json.GetValue("@odata.context").ToString(); @@ -281,17 +281,17 @@ public static TheoryDataSet QueryEnumPropertyWithFl { return new TheoryDataSet { - { "application/json;odata.metadata=full", 1, "Execute", "fulltime, parttime" }, - { "application/json;odata.metadata=minimal", 1, "Execute", "fulltime, parttime" }, - { "application/json;odata.metadata=none", 1, "Execute", "fulltime, parttime" }, + { "application/json;odata.metadata=full", 1, "Execute", "full time, Part Time" }, + { "application/json;odata.metadata=minimal", 1, "Execute", "full time, Part Time" }, + { "application/json;odata.metadata=none", 1, "Execute", "full time, Part Time" }, { "application/json;odata.metadata=full", 2, "Read", "contract" }, { "application/json;odata.metadata=minimal", 2, "Read", "contract" }, { "application/json;odata.metadata=none", 2, "Read", "contract" }, - { "application/json;odata.metadata=full", 3, "Read, Write", "fulltime, parttime, intern" }, - { "application/json;odata.metadata=minimal", 3, "Read, Write", "fulltime, parttime, intern" }, - { "application/json;odata.metadata=none", 3, "Read, Write", "fulltime, parttime, intern" }, + { "application/json;odata.metadata=full", 3, "Read, Write", "full time, Part Time, intern" }, + { "application/json;odata.metadata=minimal", 3, "Read, Write", "full time, Part Time, intern" }, + { "application/json;odata.metadata=none", 3, "Read, Write", "full time, Part Time, intern" }, }; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataEnumDeserializerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataEnumDeserializerTests.cs index 8b3083311..23067027a 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataEnumDeserializerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataEnumDeserializerTests.cs @@ -146,10 +146,13 @@ public async Task ReadAsync_Works_ForModelAlias(string content, Level expectedLe } [Theory] + [InlineData("{\"@odata.type\":\"#NS.day\",\"value\":\"mon\"}", Day.Monday)] [InlineData("{\"@odata.type\":\"#NS.day\",\"value\":\"monday\"}", Day.Monday)] [InlineData("{\"@odata.type\":\"#NS.day\",\"value\":\"Monday\"}", Day.Monday)] - [InlineData("{\"@odata.type\":\"#NS.day\",\"value\":\"monday, tuesday\"}", Day.Monday | Day.Tuesday)] - [InlineData("{\"@odata.type\":\"#NS.day\",\"value\":\"Monday, tuesday\"}", Day.Monday | Day.Tuesday)] + [InlineData("{\"@odata.type\":\"#NS.day\",\"value\":\" monday, friday \"}", Day.Monday | Day.Friday)] + [InlineData("{\"@odata.type\":\"#NS.day\",\"value\":\"mon, fri\"}", Day.Monday | Day.Friday)] + [InlineData("{\"@odata.type\":\"#NS.day\",\"value\":\"monday, tuesday \"}", Day.Monday | Day.Tuesday)] + [InlineData("{\"@odata.type\":\"#NS.day\",\"value\":\" Monday, tuesday \"}", Day.Monday | Day.Tuesday)] [InlineData("{\"@odata.type\":\"#NS.day\",\"value\":\"Monday, Tuesday\"}", Day.Monday | Day.Tuesday)] [InlineData("{\"@odata.type\":\"#NS.day\",\"value\":\"monday, tuesday, thursday\"}", Day.Monday | Day.Tuesday | Day.Thursday)] [InlineData("{\"@odata.type\":\"#NS.day\",\"value\":\"Monday, Tuesday, thursday\"}", Day.Monday | Day.Tuesday | Day.Thursday)] @@ -158,8 +161,6 @@ public async Task ReadAsync_Works_ForModelAlias(string content, Level expectedLe [InlineData("{\"@odata.type\":\"#NS.day\",\"value\":\"Monday, Friday\"}", Day.Monday | Day.Friday)] [InlineData("{\"@odata.type\":\"#NS.day\",\"value\":\"Monday, tuesday, fri\"}", Day.Monday | Day.Tuesday | Day.Friday)] [InlineData("{\"@odata.type\":\"#NS.day\",\"value\":\"Monday, fri, Wednesday\"}", Day.Monday | Day.Friday | Day.Wednesday)] - [InlineData("{\"@odata.type\":\"#NS.day\",\"value\":\"Monday, fri\"}", Day.Monday | Day.Friday)] - [InlineData("{\"@odata.type\":\"#NS.day\",\"value\":\"monday, fri\"}", Day.Monday | Day.Friday)] public async Task ReadAsync_Works_ForModelAliasWithFlags(string content, Day expectedDay) { // Arrange @@ -224,7 +225,7 @@ public enum Level [DataContract(Name = "day")] public enum Day { - [EnumMember(Value = "monday")] + [EnumMember(Value = "mon")] Monday = 1, [EnumMember(Value = "tuesday")] From b038cb914b761cb452beeb264d3ed281f3ba8949 Mon Sep 17 00:00:00 2001 From: Samuel Wanjohi Date: Sat, 14 Dec 2024 20:31:09 +0300 Subject: [PATCH 09/11] Refactor --- .../Formatter/Deserialization/ODataEnumDeserializer.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs index 6dbd4970f..5a099aa8e 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs @@ -94,7 +94,7 @@ public override object ReadInline(object item, IEdmTypeReference edmType, ODataD { if (enumValue != null) { - IEdmEnumMember enumMember = enumType.Members.FirstOrDefault(m => m.Name.Equals(enumValue.Value, StringComparison.InvariantCultureIgnoreCase)); + IEdmEnumMember enumMember = enumType.Members.FirstOrDefault(m => m.Name == enumValue.Value); if (enumMember != null) { var clrMember = memberMapAnnotation.GetClrEnumMember(enumMember); @@ -128,9 +128,9 @@ public override object ReadInline(object item, IEdmTypeReference edmType, ODataD private static object ReadFlagsEnumValue(ODataEnumValue enumValue, IEdmEnumType enumType, Type clrType, ClrEnumMemberAnnotation memberMapAnnotation) { long result = 0; - Type clrEnumType = TypeHelper.GetUnderlyingTypeOrSelf(clrType); - ReadOnlySpan source = enumValue.Value.AsSpan().Trim(); + clrType = TypeHelper.GetUnderlyingTypeOrSelf(clrType); + ReadOnlySpan source = enumValue.Value.AsSpan().Trim(); int start = 0; while (start < source.Length) { @@ -144,7 +144,7 @@ private static object ReadFlagsEnumValue(ODataEnumValue enumValue, IEdmEnumType // Extract the current value. ReadOnlySpan currentValue = source[start..end].Trim(); - bool parsed = Enum.TryParse(clrEnumType, currentValue, true, out object enumMemberParsed); + bool parsed = Enum.TryParse(clrType, currentValue, true, out object enumMemberParsed); if (parsed) { result |= Convert.ToInt64((Enum)enumMemberParsed); @@ -181,6 +181,6 @@ private static object ReadFlagsEnumValue(ODataEnumValue enumValue, IEdmEnumType start = end + 1; } - return result == 0 ? null : Enum.ToObject(clrEnumType, result); + return result == 0 ? null : Enum.ToObject(clrType, result); } } From 9781cd0e74416b2eda175ba30f6ce6376b751b00 Mon Sep 17 00:00:00 2001 From: Samuel Wanjohi Date: Sat, 14 Dec 2024 20:33:22 +0300 Subject: [PATCH 10/11] Remove unused usings --- .../Formatter/Deserialization/ODataEnumDeserializer.cs | 1 - .../Enums/EnumsController.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs index 5a099aa8e..2c59d1151 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs @@ -6,7 +6,6 @@ //------------------------------------------------------------------------------ using System; -using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Linq; using System.Threading.Tasks; diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsController.cs index 5085324d1..6e2fbc43f 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsController.cs @@ -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; From 516620278ba4888b0a07f57a10cc47ba60e18cce Mon Sep 17 00:00:00 2001 From: Samuel Wanjohi Date: Sat, 14 Dec 2024 21:13:57 +0300 Subject: [PATCH 11/11] Check if clrEnumMember is null before Convert.ToInt64 --- .../Deserialization/ODataEnumDeserializer.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs index 2c59d1151..6dc40ea5f 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs @@ -156,7 +156,6 @@ private static object ReadFlagsEnumValue(ODataEnumValue enumValue, IEdmEnumType // - 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. - parsed = false; foreach (IEdmEnumMember enumMember in enumType.Members) { // Check if the current value matches the enum member name. @@ -164,8 +163,14 @@ private static object ReadFlagsEnumValue(ODataEnumValue enumValue, IEdmEnumType if (parsed) { Enum clrEnumMember = memberMapAnnotation.GetClrEnumMember(enumMember); - result |= Convert.ToInt64(clrEnumMember); - break; + if(clrEnumMember != null) + { + result |= Convert.ToInt64(clrEnumMember); + break; + } + + // If the enum member is not found, the value is not valid. + parsed = false; } } }