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

Fix regression causing navigation properties to be auto-expanded in typeless scenarios #1424

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

gathogojr
Copy link
Contributor

@gathogojr gathogojr commented Feb 21, 2025

Fixes #1412

Fix regression causing navigation properties to be auto-expanded in typeless scenarios

The challenge we face is that EdmEntityObject, EdmComplexObject, EdmDeltaResourceObject, EdmDeltaComplexObject, and EdmDeltaDeletedResourceObject (together with their typed internal equivalents TypedEdmEntityObject and TypedEdmComplexObject) all implemement IDelta, IDeltaSetItem, and IEdmChangedObject interfaces directly or indirectly.

Illustration:

using Microsoft.AspNetCore.OData.Deltas;
using Microsoft.AspNetCore.OData.Formatter.Value;
using Microsoft.OData.Edm;

var orderEntityType = new EdmEntityType("NS", "Order");
var addressComplexType = new EdmComplexType("NS", "Address");

var orderEntityObject = new EdmEntityObject(orderEntityType);
var addressComplexObject = new EdmComplexObject(addressComplexType);
var orderDeltaResourceObject = new EdmDeltaResourceObject(orderEntityType);
var addressDeltaComplexObject = new EdmDeltaComplexObject(addressComplexType);
var orderDeltaDeletedResourceObject = new EdmDeltaDeletedResourceObject(orderEntityType);

Console.WriteLine($"EdmEntityObject - Is IDelta: {orderEntityObject is IDelta}, Is DeltaSetItem: {orderEntityObject is IDeltaSetItem}, Is IEdmChangedObject: {orderEntityObject is IEdmChangedObject}, Is DeltaResource: {orderEntityObject.IsDeltaResource()}");
Console.WriteLine($"EdmComplexObject - Is IDelta: {addressComplexObject is IDelta}, Is DeltaSetItem: {addressComplexObject is IDeltaSetItem}, Is IEdmChangedObject: {addressComplexObject is IEdmChangedObject}, Is DeltaResource: {addressComplexObject.IsDeltaResource()}");
Console.WriteLine($"EdmDeltaResourceObject - Is IDelta: {orderDeltaResourceObject is IDelta}, Is DeltaSetItem: {orderDeltaResourceObject is IDeltaSetItem}, Is IEdmChangedObject: {orderDeltaResourceObject is IEdmChangedObject}, Is DeltaResource: {orderDeltaResourceObject.IsDeltaResource()}");
Console.WriteLine($"EdmDeltaComplexObject - Is IDelta: {addressDeltaComplexObject is IDelta}, Is DeltaSetItem: {addressDeltaComplexObject is IDeltaSetItem}, Is IEdmChangedObject: {addressDeltaComplexObject is IEdmChangedObject}, Is DeltaResource: {addressDeltaComplexObject.IsDeltaResource()}");
Console.WriteLine($"EdmDeltaDeletedResourceObject - Is IDelta: {orderDeltaDeletedResourceObject is IDelta}, Is DeltaSetItem: {orderDeltaDeletedResourceObject is IDeltaSetItem}, Is IEdmChangedObject: {orderDeltaDeletedResourceObject is IEdmChangedObject}, Is DeltaResource: {orderDeltaDeletedResourceObject.IsDeltaResource()}");

Result:

EdmEntityObject - Is IDelta: True, Is DeltaSetItem: True, Is IEdmChangedObject: True, Is DeltaResource: True
EdmComplexObject - Is IDelta: True, Is DeltaSetItem: True, Is IEdmChangedObject: True, Is DeltaResource: True
EdmDeltaResourceObject - Is IDelta: True, Is DeltaSetItem: True, Is IEdmChangedObject: True, Is DeltaResource: True
EdmDeltaComplexObject - Is IDelta: True, Is DeltaSetItem: True, Is IEdmChangedObject: True, Is DeltaResource: True
EdmDeltaDeletedResourceObject - Is IDelta: True, Is DeltaSetItem: True, Is IEdmChangedObject: True, Is DeltaResource: True

For the above reason, you can't tell by interrogating the object itself where it's a delta resource - specifically for EdmEntityObject and EdmComplexObject that can be present in both delta and non-delta payloads.

What this basically means, is that if you have a controller action like the typeless scenario below, where Customer is a navigation property, the navigation property is auto-expanded whether or not there's an $expand on the URL:

public EdmEntityObjectCollection Get()
{
    var customerObject = new EdmEntityObject(TypelessEdmModel.CustomerEntityType);
    customerObject.TrySetPropertyValue("Id", 1);
    customerObject.TrySetPropertyValue("Name", "Sue");

    var orderObject = new EdmEntityObject(TypelessEdmModel.OrderEntityType);
    orderObject.TrySetPropertyValue("Id", 1);
    orderObject.TrySetPropertyValue("Amount", 310m);
    orderObject.TrySetPropertyValue("Customer", customerObject);

    return new EdmEntityObjectCollection(
        new EdmCollectionTypeReference(
            new EdmCollectionType(
                new EdmEntityTypeReference(TypelessEdmModel.OrderEntityType, false))))
    {
        orderObject
    };
}

The buggy logic is here:

bool isDelta = graph is IDelta || graph is IEdmChangedObject;

We use the ODataResourceSerializer to serialize both delta and non-delta payloads. When the above statement is evaluated, it'll always return true for nearly all (if not all) typeless Edm objects, including those in the code sample above, that shouldn't be serialized as a delta.

Telling whether an object is a delta or non-delta resource in typeless scenarios becomes a dicey affair.
I propose in this pull request to use the serializer context (via ODataSerializerContext) to indicate whether its a delta payload being written or not.

This change fixes the regression and addresses the challenge described above.

@gathogojr gathogojr force-pushed the fix/1412-fix-nav-props-auto-expanded-regression branch from e7c76b8 to 56b8452 Compare February 24, 2025 05:23
@@ -117,6 +117,7 @@ public virtual async Task WriteDeltaObjectInlineAsync(object graph, IEdmTypeRefe
}
else
{
writeContext.IsDelta = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we using these method calls to determine when the writeContext.isDelta is set? What if someone overrides these methods? They'll have to set this property explicitly? What happens if they don't?

Copy link

@anasik anasik Feb 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ElizabethOkerio , in #1413, I solved the same bug with a simple type comparison that only modifies one function in one file.

It's practically a one-liner and I would appreciate it if someone could review it. I think it's the most viable fix for this problem. I also added some tests very specific to the reported use-case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ElizabethOkerio Following our offline discussion, I have addressed the issue by setting the property from the output formatter where the serializer context is initialized

Copy link

@anasik anasik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TypelessDataModel you created here under seems very similar to the one under Untyped. Are you sure your new tests wouldn't be well suited to that folder?

In fact, your entire Typeless folder seems very similar to the Untyped folder.

Meanwhile, the existing Typeless folder looks very different from this so adding these tests to that folder would be a drastic change.

@gathogojr gathogojr force-pushed the fix/1412-fix-nav-props-auto-expanded-regression branch from 1f6386e to 7676eb1 Compare February 28, 2025 08:25
@gathogojr
Copy link
Contributor Author

The TypelessDataModel you created here under seems very similar to the one under Untyped. Are you sure your new tests wouldn't be well suited to that folder?

In fact, your entire Typeless folder seems very similar to the Untyped folder.

Meanwhile, the existing Typeless folder looks very different from this so adding these tests to that folder would be a drastic change.

@anasik I went through the Untyped folder and I don't see a significant overlap. As a matter of fact, the Edm model in the Untyped folder is typed. In addition, the Typeless folder already had existing tests for typeless scenario

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

$select query option inside $expand is ignored
3 participants