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 type casting exceptions in 'isof' and 'cast' calls. #3117

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

WanjohiSammy
Copy link
Contributor

Issues

This pull request fixes #1744.

Description

When using unquoted type parameters in isof and cast, the operations perform as expected provided that the source type can be cast to the target type. Issues arise when casting is not possible, resulting in the following exception:

Encountered invalid type cast. '{source type}' is not assignable from '{target type}'.

Lets say you have the following data model:

namespace NS;

public class Product
{
    public int ProductID { get; set; }
    // Others properties
    public Address SupplierAddress { get; set; }
}

public class DerivedProduct : Product
{
   public string AnotherProperty { get; set; }
    // Others properties
}

public class Address
{
    // Others properties
    public string City { get; set; }
}

Executing the query isof(NS.DerivedCategory) will throw the exception:

"Encountered invalid type cast. 'NS.Category' is not assignable from 'NS.Product'."

This occurs because ODL checks if NS.Product is related to NS.Category using the CheckRelatedTo function and since the types are not related or one is not a derivative of the other, the exception is thrown.

Main changes

According to the spec_cast and spec_isof specifications, there is no indication that an exception should be thrown if casting fails. This pull request introduces the TryBindDottedIdentifierForIsOfOrCastFunctionCall method, which handles the binding of DottedIdentifierToken specifically for the isof and cast function calls without performing the CheckRelatedTo validation, thereby preventing exceptions when type casting is not possible.

Checklist (Uncheck if it is not completed)

  • Test cases added
  • Build and test with one-click build and test script passed

Additional work necessary

If documentation update is needed, please add "Docs Needed" label to the issue and provide details about the required document change in the issue.

xuzhg
xuzhg previously approved these changes Nov 12, 2024
@@ -234,7 +234,22 @@ internal QueryNode BindFunctionCall(FunctionCallToken functionCallToken)

// If there isn't, bind as Uri function
// Bind all arguments
List<QueryNode> argumentNodes = new List<QueryNode>(functionCallToken.Arguments.Select(ar => this.bindMethod(ar)));
List<QueryNode> argumentNodes = new List<QueryNode>(functionCallToken.Arguments.Select(argument =>
Copy link
Contributor

Choose a reason for hiding this comment

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

It may be better to call functionCallToken.Arguments.Select(...).ToList() than new List(...). If Arguments has a known collection size, then using ToList() could use an optimized path that avoids resizing. See:
dotnet/runtime#107560

[InlineData("cast(MyAddress,Fully.Qualified.Namespace.Employee)/WorkID eq 345")]
[InlineData("cast(null,Fully.Qualified.Namespace.Employee)/WorkID eq 345")]
[InlineData("cast('',Fully.Qualified.Namespace.Employee)/WorkID eq 345")]
public void CastFunctionWithUnquotedTypeParameter_WithIncorrectType_DoesNotThrowException(string filterQuery)
Copy link
Contributor

@habbes habbes Nov 13, 2024

Choose a reason for hiding this comment

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

What is the expected behaviour of cast() when the types are incorrect? If it doesn't throw an exception, what will happen? How is this scenario expected to be handled? How will a library like AspNetCore know that the cast is not possible? And would this be a breaking change?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is what is happening currently with cast with quoted type parameter

For cast with unquoted type parameter

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for this added context. Can you also shed light on the behaviour with respect to handling of types where a cast is not possible? Ideally, from the user's endpoint the behaviour should be the same regardless of whether they user quoted or non-quoted syntax, and regardless of our internal implementation details which you have highlighted.

So could you clarify how we handle both supported and invalid cast scenarios in both quoted and unquoted variants? In which cases are we throwing an exception? Is the exception thrown consistent? Is that the expected behaviour?

My expectation is that isof should not throw an exception when a cast is invalid, but rather evaluate to a boolean that will perform the correct filter. I think cast throwing an exception when a cast is invalid is reasonable. What is not clear to me is whether such an exception should be thrown in ODL or AspNetCore. For the case of cast I would be cautious about changing the existing behaviour to avoid introducing a breaking change.

Copy link
Contributor Author

@WanjohiSammy WanjohiSammy Nov 14, 2024

Choose a reason for hiding this comment

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

With the current behavior, ODL does not throw exception or to be more specific, it doesn't check if target type is related to source type. It only bind the quoted type parameters to constant node and return the entire token as FunctionCall token. The AspNetCoreOData, on the other end, will get the token and try to create LINQ expression. If expression is not possible, AspNetCoreOData returns null BindSingleResourceCastFunctionCall will return null expression.

Let's look at this query:

/products?$filter=cast('Microsoft.AspNetCore.OData.E2E.Tests.Cast.Category')/Name eq 'Cat

AspNetCore will try to relate Entity Product to Entity Category in BindSingleResourceCastFunctionCall and returns null expression if one is not assignable from the other.

And then CreatePropertyAccessExpression will throw an expression when trying to get the Property Name from a null expression.

image

An exception like below will be thrown:

{
    "error": {
        "code": "",
        "message": "Instance property 'Name' is not defined for type 'System.Object'"
    }
}

[InlineData("cast(MyAddress,Fully.Qualified.Namespace.Employee)/WorkID eq 345")]
[InlineData("cast(null,Fully.Qualified.Namespace.Employee)/WorkID eq 345")]
[InlineData("cast('',Fully.Qualified.Namespace.Employee)/WorkID eq 345")]
public void CastFunctionWithUnquotedTypeParameter_WithIncorrectType_DoesNotThrowException(string filterQuery)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this behaviour consistent with how cast is handled when the type param is quoted?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

[InlineData("isof(MyAddress,Fully.Qualified.Namespace.Pet1)")]
[InlineData("isof(null,Fully.Qualified.Namespace.Person)")]
[InlineData("isof('',Fully.Qualified.Namespace.Person)")]
public void IsOfFunctionsWithUnquotedTypeParameter_WithIncorrectType_DoesNotThrowException(string filterQuery)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this behaviour consistent with how isof is handled when the type param is quoted?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same applied to isof as discussed in #3117 (comment), however, the method in AspNetCore that is responsible to create LINQ is different. Here is the method Expression BindIsOf(SingleValueFunctionCallNode node, QueryBinderContext context). I will also update this method to allow cast to SingleResourceCastNode as it currently only support cast to ConstantNode

Comment on lines +465 to +470
IEdmStructuredType childStructuredType = childType as IEdmStructuredType;

if (childStructuredType == null)
{
return this.bindMethod(dottedIdentifierToken);
}
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
IEdmStructuredType childStructuredType = childType as IEdmStructuredType;
if (childStructuredType == null)
{
return this.bindMethod(dottedIdentifierToken);
}
if ( childType is IEdmStructuredType childStructuredType)
{
return this.bindMethod(dottedIdentifierToken);
}

nit: Use is operator instead

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.

Isof type parameter quotes
4 participants