Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unable to use deep update on nested collections #743

Closed
vcantor opened this issue Nov 16, 2022 · 5 comments · Fixed by #747
Closed

Unable to use deep update on nested collections #743

vcantor opened this issue Nov 16, 2022 · 5 comments · Fixed by #747
Assignees
Labels

Comments

@vcantor
Copy link

vcantor commented Nov 16, 2022

Assemblies affected
Microsoft.AspNetCore.OData Version="8.0.11"
Microsoft.OData.Core" Version="7.12.5"
Microsoft.OData.Edm" Version="7.12.5"

Describe the bug
I am trying to implement a deep update according to Update Related Entities When Updating an Entity and I am receiving an error when patches on nested collections occur.

Reproduce steps
Make a patch request that is sending a delta update on a nested collection.

Data Model

public class OrderDTO
    {
        public string? Name { get; set; }
        public long Id { get; set; }
        public DateTime? Date { get; set; }
        public long? CustomerId { get; set; }
        public decimal? NetPrice { get; set; }
        public OrderItemDTO[] Items { get; set; }
    }

Controller:

 [ApiController]
    public class OrderController : ODataController
    {

        public OrderController(ILogger<OrderController> logger)
        {
            Logger = logger;
        }

        public ILogger<OrderController> Logger { get; }

        private static Task<IEnumerable<OrderDTO>> GetData()
        {
            return Task.FromResult(Enumerable.Range(1, 20).Select(id => new OrderDTO()
            {
                CustomerId = id,
                Id = id,
                Date = DateTime.UtcNow.AddDays(id),
                Name = "test",
                NetPrice = Convert.ToDecimal(Random.Shared.NextDouble()),
                Items = Enumerable.Range(1, 20).Select(itemId => new OrderItemDTO()
                {
                    Id = itemId,
                    OrderId = id,
                    Price = Convert.ToDecimal(Random.Shared.NextDouble())
                }).ToArray()
            }));
        }

        [HttpGet("v1/Orders")]
        [EnableQuery]
        public async Task<IEnumerable<OrderDTO>> GetAll()
        {
            return await GetData();
        }



        [HttpPatch("v1/Orders/{orderId}")]
        [EnableQuery]
        public async Task<IEnumerable<OrderDTO>> PatchOrder([FromRoute] long orderId, [FromBody] Delta<OrderDTO> patchedOrder)
        {
            Logger.LogInformation("Patched Instance for orderid {orderId} is {@patched}", orderId, JsonSerializer.Serialize(patchedOrder.GetInstance()));
            return await Task.FromResult(new List<OrderDTO>() { patchedOrder.GetInstance() });
        }


        [HttpPatch("v1/Orders/{orderId}/items")]
        [EnableQuery]
        public async void PatchOrderItems([FromRoute] long orderId, [FromBody] DeltaSet<OrderItemDTO> patchedOrder)
        {
            foreach (var item in patchedOrder)
            {
                if (item.Kind == DeltaItemKind.DeletedResource && item is DeltaDeletedResource<OrderItemDTO> deletedItem)
                {
                    Logger.LogInformation("Deleted Instance for orderid {orderId} is {@patched}", orderId, JsonSerializer.Serialize(deletedItem.GetInstance()));
                }
                else if (item.Kind == DeltaItemKind.Resource && item is Delta<OrderItemDTO> upsert)
                {
                    Logger.LogInformation("Patched Instance for orderid {orderId} is {@patched}", orderId, JsonSerializer.Serialize(upsert.GetInstance()));
                }
            }

        }
    }

EDM (CSDL) Model

<?xml version="1.0" encoding="utf-8"?>
  <edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
    <edmx:DataServices>
      <Schema Namespace="Demo" xmlns="http://docs.oasis-open.org/odata/ns/edm">
        <EntityType Name="OrderDTO">
          <Key>
            <PropertyRef Name="id" />
          </Key>
          <Property Name="id" Type="Edm.Int64" Nullable="false" />
          <Property Name="name" Type="Edm.String" />
          <Property Name="date" Type="Edm.DateTimeOffset" />
          <Property Name="customerId" Type="Edm.Int64" />
          <Property Name="netPrice" Type="Edm.Decimal" Scale="Variable" />
          <NavigationProperty Name="items" Type="Collection(Demo.OrderItemDTO)" />
        </EntityType>
        <EntityType Name="OrderItemDTO">
          <Key>
            <PropertyRef Name="id" />
            <PropertyRef Name="orderId" />
          </Key>
          <Property Name="orderId" Type="Edm.Int64" Nullable="false" />
          <Property Name="id" Type="Edm.Int32" Nullable="false" />
          <Property Name="price" Type="Edm.Decimal" Scale="Variable" />
        </EntityType>
        <EntityContainer Name="ODataDemo">
          <EntitySet Name="Orders" EntityType="Demo.OrderDTO">
            <NavigationPropertyBinding Path="items" Target="OrderItems" />
          </EntitySet>
          <EntitySet Name="OrderItems" EntityType="Demo.OrderItemDTO" />
        </EntityContainer>
        <Annotations Target="Demo.OrderItemDTO">
          <Annotation Term="Org.OData.Capabilities.V1.UpdateRestrictions">
            <Record>
              <PropertyValue Property="Updatable" Bool="true" />
              <PropertyValue Property="DeltaUpdateSupported" Bool="true" />
            </Record>
          </Annotation>
        </Annotations>
        <Annotations Target="Demo.ODataDemo/Orders">
          <Annotation Term="Org.OData.Capabilities.V1.DeepUpdateSupport">
            <Record>
              <PropertyValue Property="Supported" Bool="true" />
            </Record>
          </Annotation>
          <Annotation Term="Org.OData.Capabilities.V1.UpdateRestrictions">
            <Record>
              <PropertyValue Property="DeltaUpdateSupported" Bool="true" />
            </Record>
          </Annotation>
        </Annotations>
      </Schema>
    </edmx:DataServices>
  </edmx:Edmx>

Request/Response
Request Headers:

Connection: keep-alive
Accept: application/json;odata.metadata=minimal
Content-Type: application/json
OData-Version: 4.01
OData-MaxVersion: 4.01
Content-Length: 207
Host: localhost:5086
User-Agent: Apache-HttpClient/4.5.13 (Java/13.0.2)

Request Body:

PATCH http://localhost:5086/v1/orders/2

PATCH data:
{
	"name":"anothertest",
	"@id": "Order(2)",
	"items@delta":[
	{
		"@id": "OrderItems(2, 1)",
		"price":0
	},
	{
	"@removed":{
			"reason":"deleted"
			},
		"@id": "OrderItems(2, 2)"
		}
		]
}

[no cookies]

Response Headers:

HTTP/1.1 400 Bad Request
Content-Type: application/problem+json; charset=utf-8
Date: Wed, 16 Nov 2022 06:25:36 GMT
Server: Kestrel
Transfer-Encoding: chunked

Response Body:

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "traceId": "00-e5e58a4c6478a3ed25f2de9eeb25ad4a-435d9e53608229b5-00",
  "errors": {
    "": ["The input was not valid."],
    "patchedOrder": ["The patchedOrder field is required."]
  }
}

Expected behavior
The updates on the OrderItems should be populated in the Delta when using nested delta updates.

Additional context
When I am using the endpoint v1/Orders/{orderId}/items the DeltaSet is populated correctly. Only when nested patches occur the delta is not working.

Exception message from OData:

The value "Microsoft.AspNetCore.OData.Deltas.Delta`1[ODataDemo.Controllers.OrderItemDTO]" is not of type "ODataDemo.Controllers.OrderItemDTO" and cannot be used in this generic collection. (Parameter 'value')

Stacktrace:

   at System.Collections.Generic.List`1.System.Collections.IList.Add(Object item)
   at Microsoft.AspNetCore.OData.Formatter.Deserialization.CollectionDeserializationHelpers.AddToCollectionCore(IEnumerable items, IEnumerable collection, Type elementType, IList list, MethodInfo addMethod, ODataDeserializerContext context)
   at Microsoft.AspNetCore.OData.Formatter.Deserialization.DeserializationHelpers.SetCollectionProperty(Object resource, String propertyName, IEdmCollectionTypeReference edmPropertyType, Object value, Boolean clearCollection, ODataDeserializerContext context)
   at Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataResourceDeserializer.ApplyNestedDeltaResourceSet(IEdmProperty nestedProperty, Object resource, ODataDeltaResourceSetWrapper deltaResourceSetWrapper, ODataDeserializerContext readContext)
   at Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataResourceDeserializer.ApplyNestedProperty(Object resource, ODataNestedResourceInfoWrapper resourceInfoWrapper, IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext)
   at Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataResourceDeserializer.ApplyNestedProperties(Object resource, ODataResourceWrapper resourceWrapper, IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext)
   at Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataResourceDeserializer.ReadResource(ODataResourceWrapper resourceWrapper, IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext)
   at Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataResourceDeserializer.ReadAsync(ODataMessageReader messageReader, Type type, ODataDeserializerContext readContext)
   at Microsoft.AspNetCore.OData.Formatter.ODataInputFormatter.ReadFromStreamAsync(Type type, Object defaultValue, Uri baseAddress, ODataVersion version, HttpRequest request, IList`1 disposes)
@vcantor vcantor added the bug Something isn't working label Nov 16, 2022
@vcantor
Copy link
Author

vcantor commented Nov 16, 2022

ODataDemo.zip
Added the project as an attachment.

@xuzhg xuzhg self-assigned this Nov 16, 2022
@vcantor vcantor changed the title Unable to us deep update on nested collections Unable to use deep update on nested collections Nov 16, 2022
@xuzhg
Copy link
Member

xuzhg commented Nov 17, 2022

@vcantor thanks for reporting this. I think it's a feature gap. OData team has the re-design of the Delta request and response, see details OData/WebApi#2656. At the beginning, I thought I can port the works/codes from 7.x to 8.x. However, that works is still in review. Let me take a look and see whether I can move it forward in 8.x. Of course, it's open for contribution.

@xuzhg xuzhg added feature and removed bug Something isn't working labels Nov 17, 2022
@xuzhg xuzhg linked a pull request Nov 18, 2022 that will close this issue
@xuzhg
Copy link
Member

xuzhg commented Nov 18, 2022

@vcantor @julealgon Please review the PR #747 and share your comments.

With the PR changes and use the ODataDemo.zip

It works fine:

Delta nested collection:

image

normal nested collection:

image

@gathogojr
Copy link
Contributor

@vcantor Did @xuzhg's response help resolve your issue?

@vcantor
Copy link
Author

vcantor commented Nov 24, 2022

With the pull request that @xuzhg made the issue is fixed! Thank you @xuzhg! The DeltaSet for the nested property can be obtained using the TryGetPropertyValue method of DeltaOfT so for me the integration in the Patch is not required. Thank you for the quick fix! With this PR my issue is resolved.

@xuzhg xuzhg closed this as completed in #747 Dec 1, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants