diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs index c25943ade..6dc40ea5f 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs @@ -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; @@ -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) @@ -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); } + + /// + /// Reads the value of a flags enum. + /// + /// The OData enum value. + /// The EDM enum type. + /// The EDM enum CLR type. + /// The annotation containing the mapping of CLR enum members to EDM enum members. + /// The deserialized flags enum value. + private static object ReadFlagsEnumValue(ODataEnumValue enumValue, IEdmEnumType enumType, Type clrType, ClrEnumMemberAnnotation memberMapAnnotation) + { + long result = 0; + clrType = TypeHelper.GetUnderlyingTypeOrSelf(clrType); + + ReadOnlySpan 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 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); + } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataEnumSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataEnumSerializer.cs index 77e949498..c90a5d92f 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 static 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); + } } diff --git a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml index 2ec713bde..dd2c60d0e 100644 --- a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml +++ b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml @@ -3522,6 +3522,16 @@ + + + Reads the value of a flags enum. + + The OData enum value. + The EDM enum type. + The EDM enum CLR 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 +4661,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. diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsController.cs index abc4f6247..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; @@ -46,6 +45,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 +58,7 @@ private void InitEmployees() SkillSet=new List(), Gender=Gender.Female, AccessLevel=AccessLevel.Read, + EmployeeType = EmployeeType.Contract, FavoriteSports=new FavoriteSports() { LikeMost=Sport.Pingpong, @@ -70,6 +71,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 +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) { diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsDataModel.cs index 795999463..08eea53c5 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 = "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, 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..82e499a1c 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("full time, Part Time", 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", "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", "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" }, + }; + } + } + + [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(expectedEmployeeTypeValue, employeeType); + + 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..23067027a 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,48 @@ 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\":\"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, 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)] + [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)] + 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 +220,24 @@ public enum Level [EnumMember(Value = "veryhigh")] High } + + [Flags] + [DataContract(Name = "day")] + public enum Day + { + [EnumMember(Value = "mon")] + Monday = 1, + + [EnumMember(Value = "tuesday")] + Tuesday = 2, + + [EnumMember(Value = "wednesday")] + Wednesday = 4, + + [EnumMember(Value = "thursday")] + Thursday = 8, + + [EnumMember(Value = "fri")] + Friday = 16 + } }