From 285814fa6164416b3a5b386ee8b6b1ea8a7cc41b Mon Sep 17 00:00:00 2001 From: Sam Xu Date: Mon, 2 Oct 2023 13:38:28 -0700 Subject: [PATCH 1/2] Fixes #1064, enable to write dynamic properties for changed/delta objects --- .../Serialization/ODataResourceSerializer.cs | 3 +- .../DeltaToken/DeltaTokenControllers.cs | 123 ++++++++++++++ .../DeltaToken/DeltaTokenDataModel.cs | 43 +++++ .../DeltaToken/DeltaTokenQueryTests.cs | 159 ++++++++++++++++++ 4 files changed, 327 insertions(+), 1 deletion(-) create mode 100644 test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenControllers.cs create mode 100644 test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenDataModel.cs create mode 100644 test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenQueryTests.cs diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs index 705d0ae0b..ac8893247 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs @@ -143,7 +143,8 @@ private async Task WriteDeltaResourceAsync(object graph, ODataWriter writer, ODa { await writer.WriteStartAsync(resource).ConfigureAwait(false); await WriteDeltaComplexPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); - await WriteDeltaNavigationPropertiesAsync(selectExpandNode, resourceContext, writer); + await WriteDynamicComplexPropertiesAsync(resourceContext, writer).ConfigureAwait(false); + await WriteDeltaNavigationPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); await writer.WriteEndAsync().ConfigureAwait(false); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenControllers.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenControllers.cs new file mode 100644 index 000000000..d83c4476b --- /dev/null +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenControllers.cs @@ -0,0 +1,123 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OData.Extensions; +using Microsoft.AspNetCore.OData.Formatter.Value; +using Microsoft.AspNetCore.OData.Routing.Controllers; +using Microsoft.OData; +using Microsoft.OData.Edm; + +namespace Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken +{ + public class TestCustomersController : ODataController + { + public IActionResult Get() + { + IEdmModel model = Request.GetModel(); + + IEdmEntityType customerType = model.FindDeclaredType("Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken.TestCustomer") as IEdmEntityType; + IEdmEntityType customerWithAddressType = model.FindDeclaredType("Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken.TestCustomerWithAddress") as IEdmEntityType; + IEdmComplexType addressType = model.FindDeclaredType("Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken.TestAddress") as IEdmComplexType; + IEdmEntityType orderType = model.FindDeclaredType("Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken.TestOrder") as IEdmEntityType; + IEdmEntitySet ordersSet = model.FindDeclaredEntitySet("TestOrders") as IEdmEntitySet; + EdmChangedObjectCollection changedObjects = new EdmChangedObjectCollection(customerType); + + EdmDeltaComplexObject a = new EdmDeltaComplexObject(addressType); + a.TrySetPropertyValue("State", "State"); + a.TrySetPropertyValue("ZipCode", null); + + EdmDeltaResourceObject changedEntity = new EdmDeltaResourceObject(customerWithAddressType); + changedEntity.TrySetPropertyValue("Id", 1); + changedEntity.TrySetPropertyValue("Name", "Name"); + changedEntity.TrySetPropertyValue("Address", a); + changedEntity.TrySetPropertyValue("PhoneNumbers", new List { "123-4567", "765-4321" }); + changedEntity.TrySetPropertyValue("OpenProperty", 10); + changedEntity.TrySetPropertyValue("NullOpenProperty", null); + changedObjects.Add(changedEntity); + + EdmComplexObjectCollection places = new EdmComplexObjectCollection(new EdmCollectionTypeReference(new EdmCollectionType(new EdmComplexTypeReference(addressType, true)))); + EdmDeltaComplexObject b = new EdmDeltaComplexObject(addressType); + b.TrySetPropertyValue("City", "City2"); + b.TrySetPropertyValue("State", "State2"); + b.TrySetPropertyValue("ZipCode", 12345); + b.TrySetPropertyValue("OpenProperty", 10); + b.TrySetPropertyValue("NullOpenProperty", null); + places.Add(a); + places.Add(b); + + var newCustomer = new EdmDeltaResourceObject(customerType); + newCustomer.TrySetPropertyValue("Id", 10); + newCustomer.TrySetPropertyValue("Name", "NewCustomer"); + newCustomer.TrySetPropertyValue("FavoritePlaces", places); + changedObjects.Add(newCustomer); + + var newOrder = new EdmDeltaResourceObject(orderType); + newOrder.NavigationSource = ordersSet; + newOrder.TrySetPropertyValue("Id", 27); + newOrder.TrySetPropertyValue("Amount", 100); + changedObjects.Add(newOrder); + + var deletedCustomer = new EdmDeltaDeletedResourceObject(customerType); + deletedCustomer.Id = new Uri("7", UriKind.RelativeOrAbsolute); + deletedCustomer.Reason = DeltaDeletedEntryReason.Changed; + changedObjects.Add(deletedCustomer); + + var deletedOrder = new EdmDeltaDeletedResourceObject(orderType); + deletedOrder.NavigationSource = ordersSet; + deletedOrder.Id = new Uri("12", UriKind.RelativeOrAbsolute); + deletedOrder.Reason = DeltaDeletedEntryReason.Deleted; + changedObjects.Add(deletedOrder); + + var deletedLink = new EdmDeltaDeletedLink(customerType); + deletedLink.Source = new Uri("http://localhost/odata/TestCustomers(1)"); + deletedLink.Target = new Uri("http://localhost/odata/TestOrders(12)"); + deletedLink.Relationship = "Orders"; + changedObjects.Add(deletedLink); + + var addedLink = new EdmDeltaLink(customerType); + addedLink.Source = new Uri("http://localhost/odata/TestCustomers(10)"); + addedLink.Target = new Uri("http://localhost/odata/TestOrders(27)"); + addedLink.Relationship = "Orders"; + changedObjects.Add(addedLink); + + return Ok(changedObjects); + } + } + + public class TestOrdersController : ODataController + { + public IActionResult Get() + { + IEdmModel model = Request.GetModel(); + IEdmComplexType addressType = model.FindDeclaredType("Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken.TestAddress") as IEdmComplexType; + IEdmEntityType orderType = model.FindDeclaredType("Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken.TestOrder") as IEdmEntityType; + EdmChangedObjectCollection changedObjects = new EdmChangedObjectCollection(orderType); + + EdmDeltaComplexObject sampleList = new EdmDeltaComplexObject(addressType); + sampleList.TrySetPropertyValue("State", "sample state"); + sampleList.TrySetPropertyValue("ZipCode", 9); + sampleList.TrySetPropertyValue("title", "sample title"); // primitive dynamic + + EdmDeltaComplexObject location = new EdmDeltaComplexObject(addressType); + location.TrySetPropertyValue("State", "State"); + location.TrySetPropertyValue("ZipCode", null); + location.TrySetPropertyValue("OpenProperty", 10); // primitive dynamic + location.TrySetPropertyValue("key-samplelist", sampleList); // complex dynamic + + EdmDeltaResourceObject changedOrder = new EdmDeltaResourceObject(orderType); + changedOrder.TrySetPropertyValue("Id", 1); + changedOrder.TrySetPropertyValue("Amount", 42); + changedOrder.TrySetPropertyValue("Location", location); + changedObjects.Add(changedOrder); + + return Ok(changedObjects); + } + } +} diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenDataModel.cs new file mode 100644 index 000000000..89b18aa26 --- /dev/null +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenDataModel.cs @@ -0,0 +1,43 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken +{ + public class TestCustomer + { + public int Id { get; set; } + public string Name { get; set; } + public int Age { get; set; } + public virtual IList PhoneNumbers { get; set; } + public virtual IList Orders { get; set; } + public virtual IList FavoritePlaces { get; set; } + public IDictionary DynamicProperties { get; set; } + } + + public class TestCustomerWithAddress : TestCustomer + { + public virtual TestAddress Address { get; set; } + } + + public class TestOrder + { + public int Id { get; set; } + public int Amount { get; set; } + + public TestAddress Location { get; set; } + } + + public class TestAddress + { + public string State { get; set; } + public string City { get; set; } + public int? ZipCode { get; set; } + public IDictionary DynamicProperties { get; set; } + } +} diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenQueryTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenQueryTests.cs new file mode 100644 index 000000000..0eff7d9bd --- /dev/null +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenQueryTests.cs @@ -0,0 +1,159 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.OData.E2E.Tests.Extensions; +using Microsoft.AspNetCore.OData.TestCommon; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OData.Edm; +using Microsoft.OData.ModelBuilder; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken +{ + public class DeltaTokenQueryTests : WebApiTestBase + { + public DeltaTokenQueryTests(WebApiTestFixture fixture) + : base(fixture) + { + } + + protected static void UpdateConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(TestCustomersController), typeof(TestOrdersController)); + services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", GetEdmModel()).Count().Filter().OrderBy().Expand().SetMaxTop(null).Select()); + } + + private static IEdmModel GetEdmModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("TestCustomers"); + builder.EntitySet("TestOrders"); + return builder.GetEdmModel(); + } + + [Fact] + public async Task DeltaVerifyReslt() + { + HttpRequestMessage get = new HttpRequestMessage(HttpMethod.Get, "odata/TestCustomers?$deltaToken=abc"); + get.Headers.Add("Accept", "application/json;odata.metadata=minimal"); + get.Headers.Add("OData-Version", "4.01"); + HttpClient client = CreateClient(); + HttpResponseMessage response = await client.SendAsync(get); + Assert.True(response.IsSuccessStatusCode); + dynamic results = await response.Content.ReadAsObject(); + + Assert.True(results.value.Count == 7, "There should be 7 entries in the response"); + + var changeEntity = results.value[0]; + Assert.True(((JToken)changeEntity).Count() == 9, "The changed customer should have 6 properties plus type written. But now it contains non-changed properties, it's regression bug?"); + string changeEntityType = changeEntity["@type"].Value as string; + Assert.True(changeEntityType != null, "The changed customer should have type written"); + Assert.True(changeEntityType.Contains("#Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken.TestCustomerWithAddress"), "The changed order should be a TestCustomerWithAddress"); + Assert.True(changeEntity.Id.Value == 1, "The ID Of changed customer should be 1."); + Assert.True(changeEntity.OpenProperty.Value == 10, "The OpenProperty property of changed customer should be 10."); + Assert.True(changeEntity.NullOpenProperty.Value == null, "The NullOpenProperty property of changed customer should be null."); + Assert.True(changeEntity.Name.Value == "Name", "The Name of changed customer should be 'Name'"); + Assert.True(((JToken)changeEntity.Address).Count() == 3, "The changed entity's Address should have 2 properties written. But now it contains non-changed properties, it's regression bug?"); + Assert.True(changeEntity.Address.State.Value == "State", "The changed customer's Address.State should be 'State'."); + Assert.True(changeEntity.Address.ZipCode.Value == (int?)null, "The changed customer's Address.ZipCode should be null."); + + var phoneNumbers = changeEntity.PhoneNumbers; + Assert.True(((JToken)phoneNumbers).Count() == 2, "The changed customer should have 2 phone numbers"); + Assert.True(phoneNumbers[0].Value == "123-4567", "The first phone number should be '123-4567'"); + Assert.True(phoneNumbers[1].Value == "765-4321", "The second phone number should be '765-4321'"); + + var newCustomer = results.value[1]; + Assert.True(((JToken)newCustomer).Count() == 5, "The new customer should have 3 properties written, But now it contains 2 non-changed properties, it's regression bug?"); + Assert.True(newCustomer.Id.Value == 10, "The ID of the new customer should be 10"); + Assert.True(newCustomer.Name.Value == "NewCustomer", "The name of the new customer should be 'NewCustomer'"); + + var places = newCustomer.FavoritePlaces; + Assert.True(((JToken)places).Count() == 2, "The new customer should have 2 favorite places"); + + var place1 = places[0]; + Assert.True(((JToken)place1).Count() == 3, "The first favorite place should have 2 properties written.But now it contains non-changed properties, it's regression bug?"); + Assert.True(place1.State.Value == "State", "The first favorite place's state should be 'State'."); + Assert.True(place1.ZipCode.Value == (int?)null, "The first favorite place's Address.ZipCode should be null."); + + var place2 = places[1]; + Assert.True(((JToken)place2).Count() == 5, "The second favorite place should have 5 properties written."); + Assert.True(place2.City.Value == "City2", "The second favorite place's Address.City should be 'City2'."); + Assert.True(place2.State.Value == "State2", "The second favorite place's Address.State should be 'State2'."); + Assert.True(place2.ZipCode.Value == 12345, "The second favorite place's Address.ZipCode should be 12345."); + Assert.True(place2.OpenProperty.Value == 10, "The second favorite place's Address.OpenProperty should be 10."); + Assert.True(place2.NullOpenProperty.Value == null, "The second favorite place's Address.NullOpenProperty should be null."); + + var newOrder = results.value[2]; + Assert.True(((JToken)newOrder).Count() == 4, "The new order should have 2 properties plus context written, , But now it contains one non-changed properties, it's regression bug?"); + string newOrderContext = newOrder["@context"].Value as string; + Assert.True(newOrderContext != null, "The new order should have a context written"); + Assert.True(newOrderContext.Contains("$metadata#TestOrders"), "The new order should come from the TestOrders entity set"); + Assert.True(newOrder.Id.Value == 27, "The ID of the new order should be 27"); + Assert.True(newOrder.Amount.Value == 100, "The amount of the new order should be 100"); + + var deletedEntity = results.value[3]; + Assert.True(deletedEntity["@id"].Value == "7", "The ID of the deleted customer should be 7"); + Assert.True(deletedEntity["@removed"].reason.Value == "changed", "The reason for the deleted customer should be 'changed'"); + + var deletedOrder = results.value[4]; + string deletedOrderContext = deletedOrder["@context"].Value as string; + Assert.True(deletedOrderContext != null, "The deleted order should have a context written"); + Assert.True(deletedOrderContext.Contains("$metadata#TestOrders"), "The deleted order should come from the TestOrders entity set"); + Assert.True(deletedOrder["@id"].Value == "12", "The ID of the deleted order should be 12"); + Assert.True(deletedOrder["@removed"].reason.Value == "deleted", "The reason for the deleted order should be 'deleted'"); + + var deletedLink = results.value[5]; + Assert.True(deletedLink.source.Value == "http://localhost/odata/TestCustomers(1)", "The source of the deleted link should be 'http://localhost/odata/TestCustomers(1)'"); + Assert.True(deletedLink.target.Value == "http://localhost/odata/TestOrders(12)", "The target of the deleted link should be 'http://localhost/odata/TestOrders(12)'"); + Assert.True(deletedLink.relationship.Value == "Orders", "The relationship of the deleted link should be 'Orders'"); + + var addedLink = results.value[6]; + Assert.True(addedLink.source.Value == "http://localhost/odata/TestCustomers(10)", "The source of the added link should be 'http://localhost/odata/TestCustomers(10)'"); + Assert.True(addedLink.target.Value == "http://localhost/odata/TestOrders(27)", "The target of the added link should be 'http://localhost/odata/TestOrders(27)'"); + Assert.True(addedLink.relationship.Value == "Orders", "The relationship of the added link should be 'Orders'"); + } + + [Fact] + public async Task DeltaVerifyReslt_ContainsDynamicComplexProperties() + { + HttpRequestMessage get = new HttpRequestMessage(HttpMethod.Get, "odata/TestOrders?$deltaToken=abc"); + get.Headers.Add("Accept", "application/json;odata.metadata=minimal"); + get.Headers.Add("OData-Version", "4.01"); + HttpClient client = CreateClient(); + HttpResponseMessage response = await client.SendAsync(get); + Assert.True(response.IsSuccessStatusCode); + + string result = await response.Content.ReadAsStringAsync(); + Assert.Equal("{\"@context\":\"http://localhost/odata/$metadata#TestOrders/$delta\"," + + "\"value\":[" + + "{" + + "\"Id\":1," + + "\"Amount\":42," + + "\"Location\":{" + + "\"State\":\"State\"," + + "\"City\":null," + + "\"ZipCode\":null," + + "\"OpenProperty\":10," + + "\"key-samplelist\":{" + + "\"@type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken.TestAddress\"," + + "\"State\":\"sample state\"," + + "\"City\":null," + + "\"ZipCode\":9," + + "\"title\":\"sample title\"" + + "}" + + "}" + + "}" + + "]" + + "}", + result); + } + } +} From 8959e1034957134d35ab8dcd4b68c8f8995ed52c Mon Sep 17 00:00:00 2001 From: Sam Xu Date: Mon, 2 Oct 2023 13:40:13 -0700 Subject: [PATCH 2/2] Change the copy right --- .../DeltaToken/DeltaTokenControllers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenControllers.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenControllers.cs index d83c4476b..dfc61306c 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenControllers.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenControllers.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// +// // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. //