AspNetCoreOData
AspNetCoreOData copied to clipboard
Delta Patch doesn't work for nested abstract complex types
Assemblies affected ASP.NET Core OData 8.0.10
Describe the bug
Look at the model below. There is an Odata controller for SomeSystem. When trying to Patch a SomeSystem, but when chaning the type of the nested Connection to another inheritor of ErpConnection, the patch fails.
Reproduce steps Made a small project to replicate this issue: Reproduction
- Post an object like the one in 'Request/Response'>POST to "odata/SomeSystem" (with a nested object of type
SapConnection : ErpConnection). This will return the added object with a GUID - Patch that object like the one in 'Request/Response'>PATCH to "odata/SomeSystem/{key} (changing the nested object to be of type
D365Connection : ErpConnection). Use the key from step 1. This will fail with the following error:System.ArgumentException: Cannot use Delta of type 'DeltaPatchIssue.Models.D365Connection' on an entity of type 'DeltaPatchIssue.Models.SapConnection'. (Parameter 'original')Full stack trace is at the bottom.
It seems this issue was fixed in the AspNet OData with #2572, but not in AspNetCore version, or I might have misundestood it.
Data Model

[DataContract]
public class SomeSystem
{
[Key]
[DataMember(Name = "Key")] public string? Key { get; set; }
[DataMember(Name = "Name")] public string? Name { get; set; }
[DataMember(Name = "Connection")] public ErpConnection? Connection { get; set; }
}
public abstract class ErpConnection
{
[DataMember(Name = "ConnectionName")] public string? ConnectionName { get; set; }
}
[DataContract]
public class SapConnection : ErpConnection
{
[DataMember(Name = "SapConnectionUrl")]
public string? SapConnectionUrl { get; set; }
}
[DataContract]
public class D365Connection : ErpConnection
{
[DataMember(Name = "D365ConnectionUrl")]
public string? D365ConnectionUrl { get; set; }
}
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="DeltaPatchIssue.Models" xmlns="http://docs.oasis-open.org/odata/ns/edm">
<EntityType Name="SomeSystem">
<Key>
<PropertyRef Name="Key" />
</Key>
<Property Name="Key" Type="Edm.String" Nullable="false" />
<Property Name="Name" Type="Edm.String" />
<Property Name="Connection" Type="DeltaPatchIssue.Models.ErpConnection" />
</EntityType>
<ComplexType Name="ErpConnection" Abstract="true">
<Property Name="ConnectionName" Type="Edm.String" />
</ComplexType>
<ComplexType Name="SapConnection" BaseType="DeltaPatchIssue.Models.ErpConnection">
<Property Name="SapConnectionUrl" Type="Edm.String" />
</ComplexType>
<ComplexType Name="D365Connection" BaseType="DeltaPatchIssue.Models.ErpConnection">
<Property Name="D365ConnectionUrl" Type="Edm.String" />
</ComplexType>
</Schema>
<Schema Namespace="Default" xmlns="http://docs.oasis-open.org/odata/ns/edm">
<EntityContainer Name="Container">
<EntitySet Name="System" EntityType="DeltaPatchIssue.Models.SomeSystem" />
</EntityContainer>
</Schema>
</edmx:DataServices>
</edmx:Edmx>
Request/Response POST
{
"name": "SomeSapSystem",
"connection": {
"@odata.type": "#DeltaPatchIssue.Models.SapConnection",
"SapConnectionUrl": "http://something.com",
"connectionName": "SapConnection"
}
}
PATCH
{
"Name": "SomeD365System",
"Connection": {
"@odata.type": "#DeltaPatchIssue.Models.D365Connection",
"D365ConnectionUrl": "http://something.com",
"ConnectionName": "Not a SapConnection anymore"
}
}
Expected behavior The object should be patched with the new nested object.
Screenshots If applicable, add screenshots to help explain your problem.
Additional context
System.ArgumentException: Cannot use Delta of type 'DeltaPatchIssue.Models.D365Connection' on an entity of type 'DeltaPatchIssue.Models.SapConnection'. (Parameter 'original')
at Microsoft.AspNetCore.OData.Deltas.Delta`1.CopyChangedValues(T original)
at CallSite.Target(Closure , CallSite , Object , Object )
at Microsoft.AspNetCore.OData.Deltas.Delta`1.CopyChangedValues(T original)
at Microsoft.AspNetCore.OData.Deltas.Delta`1.Patch(T original)
at DeltaPatchIssue.Cache.CacheService.Patch(String key, Delta`1 system) in C:\git\DeltaPatchIssue\DeltaPatchIssue\Cache\CacheService.cs:line 34
at DeltaPatchIssue.Controllers.SomeSystemController.Patch(String key, Delta`1 system) in C:\git\DeltaPatchIssue\DeltaPatchIssue\Controllers\SomeSystemController.cs:line 48
at lambda_method4(Closure , Object , Object[] )
at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeActionMethodAsync()
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeNextActionFilterAsync()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
HEADERS
=======
Accept: */*
Connection: keep-alive
Host: localhost:7119
User-Agent: PostmanRuntime/7.29.0
Accept-Encoding: gzip, deflate, br
Content-Type: application/json
Content-Length: 220
Postman-Token: eb837b3a-252e-42fc-b6a3-90d817bd038f
Duplicate of https://github.com/OData/WebApi/issues/2586 @PaulBujor Thank you for reporting the issue. We already have issue #2586 in our backlog. #2572 fixed a different but related issue.
Hey @gathogojr, thanks for the reply. Not entirely sure how the relationship between the two repos is. Will fixed in WebApi be carried over to AspNetCoreOdata?
Marked as regression from 7.x.
@xuzhg -- it doesn't look like Sreejith's https://github.com/OData/WebApi/issues/2572 got ported to ASPNetCore.OData. Can you make sure it gets fixed here?