Skip to content

Nested Delta<{Complex Type}> in a typed delta payload throws serialization exception #1425

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

Open
gathogojr opened this issue Feb 25, 2025 · 0 comments
Assignees
Labels
bug Something isn't working P2

Comments

@gathogojr
Copy link
Contributor

gathogojr commented Feb 25, 2025

Assemblies affected

  • ASP.NET Core OData 8.x
  • ASP.NET Core OData 9.x

Describe the bug
When a typed delta response payload contains a nested Delta<{Complex Type}>, a serialization exception is thrown

Reproduce steps
Consider a simple OData service that processes a delta payload comprising of the following:

Data model:

namespace SampleNs.Models
{
    public class Customer
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public Address StreetAddress { get; set; }
    }

    public class Address
    {
        public string City { get; set; }
    }
}

_Controller:_

```csharp
namespace SampleNs.Controllers
{
    public class CustomersController : ODataController
    {
        public ActionResult PatchAsync([FromBody] DeltaSet<Customer> deltaSet)
        {
            return Ok(deltaSet);
        }
    }
}

Service Configuration:

var builder = WebApplication.CreateBuilder(args);

var modelBuilder = new ODataConventionModelBuilder();
modelBuilder.EntitySet<Customer>("Customers");

builder.Services.AddControllers().AddOData(
    options => options.EnableQueryFeatures().AddRouteComponents(
        modelBuilder.GetEdmModel()));

var app = builder.Build();

app.UseRouting();
app.MapControllers();

app.Run();

EDM (CSDL) Model

This XML file does not appear to have any style information associated with it. The document tree is shown below.
<edmx:Edmx xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx" Version="4.0">
    <edmx:DataServices>
        <Schema xmlns="http://docs.oasis-open.org/odata/ns/edm" Namespace="SampleNs.Models">
            <EntityType Name="Customer">
                <Key>
                    <PropertyRef Name="Id"/>
                </Key>
                <Property Name="Id" Type="Edm.Int32" Nullable="false"/>
                <Property Name="Name" Type="Edm.String" Nullable="false"/>
                <Property Name="StreetAddress" Type="SampleNs.Models.Address" Nullable="false"/>
            </EntityType>
            <ComplexType Name="Address">
                <Property Name="City" Type="Edm.String" Nullable="false"/>
            </ComplexType>
        </Schema>
        <Schema xmlns="http://docs.oasis-open.org/odata/ns/edm" Namespace="Default">
            <EntityContainer Name="Container">
                <EntitySet Name="Customers" EntityType="SampleNs.Models.Customer"/>
            </EntityContainer>
        </Schema>
        </edmx:DataServices>
</edmx:Edmx>

Request/Response
Uri:

PATCH http://localhost:5013/Customers

Body:

{
  "@odata.context": "http://localhost:5013/$metadata#Customers/$delta",
  "value": [
    {
      "Id": 1,
      "StreetAddress": {
        "City": "London"
      }
    }
  ]
}

Exception:

System.InvalidOperationException: The EDM instance of type '[SampleNs.Models.Address Nullable=True]' is missing the property 'City'.
at Microsoft.AspNetCore.OData.Formatter.ResourceContext.GetPropertyValue(String propertyName)
at Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.CreateStructuralProperty(IEdmStructuralProperty structuralProperty, ResourceContext resourceContext)
at Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.CreateStructuralPropertyBag(SelectExpandNode selectExpandNode, ResourceContext resourceContext)
at Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.CreateResource(SelectExpandNode selectExpandNode, ResourceContext resourceContext)
at Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.WriteResourceAsync(Object graph, ODataWriter writer, ODataSerializerContext writeContext, IEdmTypeReference expectedType)
at Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.WriteDeltaObjectInlineAsync(Object graph, IEdmTypeReference expectedType, ODataWriter writer, ODataSerializerContext writeContext)
at Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.WriteDeltaComplexAndExpandedNavigationPropertyAsync(IEdmProperty edmProperty, SelectItem selectItem, ResourceContext resourceContext, ODataWriter writer, Type navigationPropertyType)
at Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.WriteDeltaComplexPropertiesAsync(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer)
at Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.WriteResourceContent(ODataWriter writer, SelectExpandNode selectExpandNode, ResourceContext resourceContext, Boolean isDelta)
at Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.WriteResourceAsync(Object graph, ODataWriter writer, ODataSerializerContext writeContext, IEdmTypeReference expectedType)
at Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.WriteDeltaObjectInlineAsync(Object graph, IEdmTypeReference expectedType, ODataWriter writer, ODataSerializerContext writeContext)
at Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeltaResourceSetSerializer.WriteDeltaResourceSetAsync(IEnumerable enumerable, IEdmTypeReference feedType, ODataWriter writer, ODataSerializerContext writeContext)
at Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeltaResourceSetSerializer.WriteObjectInlineAsync(Object graph, IEdmTypeReference expectedType, ODataWriter writer, ODataSerializerContext writeContext)
at Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeltaResourceSetSerializer.WriteObjectAsync(Object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext)
at Microsoft.AspNetCore.OData.Formatter.ODataOutputFormatterHelper.WriteToStreamAsync(Type type, Object value, IEdmModel model, ODataVersion version, Uri baseAddress, MediaTypeHeaderValue contentType, HttpRequest request, IHeaderDictionary requestHeaders, IODataSerializerProvider serializerProvider)
at Microsoft.AspNetCore.OData.Formatter.ODataOutputFormatterHelper.WriteToStreamAsync(Type type, Object value, IEdmModel model, ODataVersion version, Uri baseAddress, MediaTypeHeaderValue contentType, HttpRequest request, IHeaderDictionary requestHeaders, IODataSerializerProvider serializerProvider)
at Microsoft.AspNetCore.OData.Formatter.ODataOutputFormatter.WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Logged|22_0(ResourceInvoker invoker, IActionResult result)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|28_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Logged|17_1(ResourceInvoker invoker)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Logged|17_1(ResourceInvoker invoker)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware: Warning: The response has already started, the error page middleware will not be executed.
Microsoft.AspNetCore.Server.Kestrel: Error: Connection id "0HNALEG08BM4D", Request id "0HNALEG08BM4D:00000001": An unhandled exception was thrown by the application.

Expected behavior
Expected response to be returned:

{
  "@odata.context": "http://localhost:5013/$metadata#Customers/$delta",
  "value": [
    {
      "Id": 1,
      "StreetAddress": {
        "City": "London"
      }
    }
  ]
}

Screenshots
Deserialization is successful:

Image

Additional context
The issue is related to this line here:

navigationPropertyType is passed into the function and it has a default value of null.
For single-valued delta complex properties, it's not passed

ODataSerializerContext.Type is used by IsDeltaOf property to check if we're dealing with a delta at this point:

if (SerializerContext.IsDeltaOfT && ResourceInstance is IDelta delta && delta.TryGetPropertyValue(propertyName, out object value))

So what will happen is that since Type will be null, the logic in ResourceContext.GetPropertyValue breaks and an exception is thrown that the property couldn't be found on the type, even when the property exists.

Changing the line I referenced as follows fixes the issue.

nestedWriteContext.Type = navigationPropertyType ?? propertyValue.GetType();

This works because we're able to later evaluate that it's a DeltaOfT object.

We need to determine if this is the best way to fix the issue. There could be room for further improvement. We call deltaNestedProperties.TryGetValue method to retrieve the value for the nested resource here:

if (changedProperties != null && changedProperties.Contains(navigationProperty.Name) && deltaNestedProperties.TryGetValue(navigationProperty.Name, out object obj))

When then call obj.GetType() to get the type of the value, that type we later pass onto WriteDeltaComplexAndExpandedNavigationPropertyAsync, where we later make a second call to retrieve the nested resource value at this point:

object propertyValue = resourceContext.GetPropertyValue(edmProperty.Name);
.

The property value that is returned would give us the same type that was passed into the method...
It's not clear why only the navigation property type is passed, and why this was not expanded to {nested resource type} so it covers both complex and navigation properties.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working P2
Projects
None yet
Development

No branches or pull requests

2 participants