diff --git a/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.cs b/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.cs index 9b590d4f..4d517587 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.cs @@ -642,7 +642,17 @@ public virtual Expression BindSingleResourceCastFunctionCall(SingleResourceFunct IEdmModel model = context.Model; - string targetEdmTypeName = (string)((ConstantNode)node.Parameters.Last()).Value; + string targetEdmTypeName = null; + QueryNode queryNode = node.Parameters.Last(); + if (queryNode is ConstantNode constantNode) + { + targetEdmTypeName = constantNode.Value as string; + } + else if (queryNode is SingleResourceCastNode singleResourceCastNode) + { + targetEdmTypeName = singleResourceCastNode.TypeReference.FullName(); + } + IEdmType targetEdmType = model.FindType(targetEdmTypeName); Type targetClrType = null; diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/EntityTypeTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/EntityTypeTests.cs index e6c0db2b..be9f3424 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/EntityTypeTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/EntityTypeTests.cs @@ -55,7 +55,8 @@ public async Task EntityTypeSerializesAsODataEntry() "\"BaseSalary\":0," + "\"Birthday\":\"2020-09-10T01:02:03Z\"," + "\"WorkCompanyId\":0," + - "\"HomeAddress\":null" + + "\"HomeAddress\":null," + + "\"Location\":null" + "}", actual); } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Models/Employee.cs b/test/Microsoft.AspNetCore.OData.Tests/Models/Employee.cs index 1420d848..6cd156fe 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Models/Employee.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Models/Employee.cs @@ -32,6 +32,8 @@ public class Employee public Address HomeAddress { get; set; } + public Address Location { get; set; } + public IList DirectReports { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Models/WorkAddress.cs b/test/Microsoft.AspNetCore.OData.Tests/Models/WorkAddress.cs new file mode 100644 index 00000000..50c73cb0 --- /dev/null +++ b/test/Microsoft.AspNetCore.OData.Tests/Models/WorkAddress.cs @@ -0,0 +1,14 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNetCore.OData.Tests.Models +{ + internal class WorkAddress : Address + { + public string OfficeNumber { get; set; } + } +} diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/MockEdmNodesHelper.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/MockEdmNodesHelper.cs new file mode 100644 index 00000000..c6751402 --- /dev/null +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/MockEdmNodesHelper.cs @@ -0,0 +1,81 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; + +namespace Microsoft.AspNetCore.OData.Tests.Query.Expressions +{ + public class MockSingleEntityNode : SingleEntityNode + { + private readonly IEdmEntityTypeReference typeReference; + private readonly IEdmEntitySetBase set; + + public MockSingleEntityNode(IEdmEntityTypeReference type, IEdmEntitySetBase set) + { + this.typeReference = type; + this.set = set; + } + + public override IEdmTypeReference TypeReference + { + get { return this.typeReference; } + } + + public override IEdmNavigationSource NavigationSource + { + get { return this.set; } + } + + public override IEdmStructuredTypeReference StructuredTypeReference + { + get { return this.typeReference; } + } + + public override IEdmEntityTypeReference EntityTypeReference + { + get { return this.typeReference; } + } + + public static MockSingleEntityNode CreateFakeNodeForEmployee() + { + var employeeType = HardCodedTestModel.GetEntityType("Microsoft.AspNetCore.OData.Tests.Models.Employee"); + return new MockSingleEntityNode(HardCodedTestModel.GetEntityTypeReference(employeeType), HardCodedTestModel.GetEmployeeSet()); + } + } + + public class MockCollectionResourceNode : CollectionResourceNode + { + private readonly IEdmStructuredTypeReference _typeReference; + private readonly IEdmNavigationSource _source; + private readonly IEdmTypeReference _itemType; + private readonly IEdmCollectionTypeReference _collectionType; + + public MockCollectionResourceNode(IEdmStructuredTypeReference type, IEdmNavigationSource source, IEdmTypeReference itemType, IEdmCollectionTypeReference collectionType) + { + _typeReference = type; + _source = source; + _itemType = itemType; + _collectionType = collectionType; + } + + public override IEdmStructuredTypeReference ItemStructuredType => _typeReference; + + public override IEdmNavigationSource NavigationSource => _source; + + public override IEdmTypeReference ItemType => _itemType; + + public override IEdmCollectionTypeReference CollectionType => _collectionType; + + public static MockCollectionResourceNode CreateFakeNodeForEmployee() + { + var singleEntityNode = MockSingleEntityNode.CreateFakeNodeForEmployee(); + return new MockCollectionResourceNode( + singleEntityNode.EntityTypeReference, singleEntityNode.NavigationSource, singleEntityNode.EntityTypeReference, singleEntityNode.EntityTypeReference.AsCollection()); + } + } +} diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/QueryBinderTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/QueryBinderTests.cs index 6750f18f..51d85321 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/QueryBinderTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/QueryBinderTests.cs @@ -6,15 +6,20 @@ //------------------------------------------------------------------------------ using System; +using System.Collections.Generic; using System.Linq.Expressions; using System.Reflection; +using Moq; +using Xunit; +using Microsoft.AspNetCore.OData.Edm; +using Microsoft.AspNetCore.OData.Query; using Microsoft.AspNetCore.OData.Query.Expressions; using Microsoft.AspNetCore.OData.Query.Wrapper; using Microsoft.AspNetCore.OData.Tests.Commons; using Microsoft.OData.Edm; +using Microsoft.OData.ModelBuilder; using Microsoft.OData.UriParser; -using Moq; -using Xunit; +using Microsoft.AspNetCore.OData.Tests.Models; namespace Microsoft.AspNetCore.OData.Tests.Query.Expressions; @@ -100,6 +105,135 @@ public void BindSingleResourceFunctionCallNode_ThrowsNotSupported_ForNotAcceptNo "Unknown function 'anyUnknown'."); } + [Theory] + [InlineData(typeof(ConstantNode))] + [InlineData(typeof(SingleResourceCastNode))] + public void BindSingleResourceFunctionCallNode_CastingEntityType_ReturnsExpression(Type queryNodeType) + { + // Arrange + var binder = new MyQueryBinder(); + + var model = HardCodedTestModel.TestModel; + + // Create the type reference and navigation source + var employeeType = HardCodedTestModel.GetEntityType("Microsoft.AspNetCore.OData.Tests.Models.Employee"); + var employeeTypeRef = HardCodedTestModel.GetEntityTypeReference(employeeType); + var collectionNode = MockCollectionResourceNode.CreateFakeNodeForEmployee(); + + // Get the entity type for the Manager entity -> Manager is derived from Employee + var managerType = HardCodedTestModel.GetEntityType("Microsoft.AspNetCore.OData.Tests.Models.Manager"); + + // Create a ResourceRangeVariableReferenceNode for the Employee entity + var rangeVariable = new ResourceRangeVariable("$it", employeeTypeRef, collectionNode); + var employeeNode = new ResourceRangeVariableReferenceNode(rangeVariable.Name, rangeVariable) as SingleValueNode; + + // Create the parameters list + int capacity = 2; + var parameters = new List(capacity) + { + employeeNode // First parameter is the Person entity + }; + + if (queryNodeType == typeof(SingleResourceCastNode)) + { + // Create a SingleResourceCastNode to cast Employee to Microsoft.AspNetCore.OData.Tests.Models.Manager + var singleResourceCastNode = new SingleResourceCastNode(employeeNode as SingleResourceNode, managerType); + parameters.Add(singleResourceCastNode); // Second parameter is the SingleResourceCastNode + } + else if (queryNodeType == typeof(ConstantNode)) + { + // Create a ConstantNode to cast Employee to Microsoft.AspNetCore.OData.Tests.Models.Manager + var constantNode = new ConstantNode("Microsoft.AspNetCore.OData.Tests.Models.Manager"); + parameters.Add(constantNode); // Second parameter is the ConstantNode + } + + // Create the SingleResourceFunctionCallNode + var node = new SingleResourceFunctionCallNode("cast", parameters, collectionNode.ItemStructuredType, collectionNode.NavigationSource); + + // Create an instance of QueryBinderContext using the real model and settings + Type clrType = model.GetClrType(employeeType); + var context = new QueryBinderContext(model, new ODataQuerySettings(), clrType); + + // Act + Expression expression = binder.BindSingleResourceFunctionCallNode(node, context); + + // Assert + Assert.NotNull(expression); + Assert.Equal("($it As Manager)", expression.ToString()); // cast($it, 'Microsoft.AspNetCore.OData.Tests.Models.Manager') where $it is the Employee entity + Assert.Equal("Microsoft.AspNetCore.OData.Tests.Models.Manager", expression.Type.ToString()); + Assert.Equal(typeof(Manager), expression.Type); + Assert.Equal(ExpressionType.TypeAs, expression.NodeType); + Assert.Equal(employeeType.FullName(), (expression as UnaryExpression).Operand.Type.FullName); + Assert.Equal(managerType.FullName(), (expression as UnaryExpression).Type.FullName); + } + + [Theory] + [InlineData(typeof(ConstantNode))] + [InlineData(typeof(SingleResourceCastNode))] + public void BindSingleResourceFunctionCallNode_PropertyCasting_ReturnsExpression(Type queryNodeType) + { + // Arrange + var binder = new MyQueryBinder(); + + var model = HardCodedTestModel.TestModel; + + // Create the type reference and navigation source + var employeeType = HardCodedTestModel.GetEntityType("Microsoft.AspNetCore.OData.Tests.Models.Employee"); + var employeeTypeRef = HardCodedTestModel.GetEntityTypeReference(employeeType); + var collectionNode = MockCollectionResourceNode.CreateFakeNodeForEmployee(); + + var addressType = HardCodedTestModel.GetEdmComplexType("Microsoft.AspNetCore.OData.Tests.Models.Address"); + var workAddressType = HardCodedTestModel.GetEdmComplexType("Microsoft.AspNetCore.OData.Tests.Models.WorkAddress"); + + // Create a ResourceRangeVariableReferenceNode for the Employee entity + var rangeVariable = new ResourceRangeVariable("$it", employeeTypeRef, collectionNode); + var employeeNode = new ResourceRangeVariableReferenceNode(rangeVariable.Name, rangeVariable) as SingleValueNode; + + // Create a SingleComplexNode for the Location property of the Person entity + var locationProperty = HardCodedTestModel.GetEmployeeLocationProperty(); + var locationNode = new SingleComplexNode(employeeNode as SingleResourceNode, locationProperty); + + // Create the parameters list + int capacity = 2; + var parameters = new List(capacity) + { + locationNode // First parameter is the Location property + }; + + if(queryNodeType == typeof(SingleResourceCastNode)) + { + // Create a SingleResourceCastNode to cast Location to Microsoft.AspNetCore.OData.Tests.Models.WorkAddress + var singleResourceCastNode = new SingleResourceCastNode(locationNode, workAddressType); + parameters.Add(singleResourceCastNode); // Second parameter is the SingleResourceCastNode + } + else if (queryNodeType == typeof(ConstantNode)) + { + // Create a ConstantNode to cast Location to NS.WorkAddress + var constantNode = new ConstantNode("Microsoft.AspNetCore.OData.Tests.Models.WorkAddress"); + parameters.Add(constantNode); // Second parameter is the ConstantNode + } + + // Create the SingleResourceFunctionCallNode + var node = new SingleResourceFunctionCallNode("cast", parameters, collectionNode.ItemStructuredType, collectionNode.NavigationSource); + + // Create an instance of QueryBinderContext using the real model and settings + Type clrType = model.GetClrType(employeeType); + var context = new QueryBinderContext(model, new ODataQuerySettings(), clrType); + + // Act + Expression expression = binder.BindSingleResourceFunctionCallNode(node, context); + + // Assert + Assert.NotNull(expression); + Assert.Equal("($it.Location As WorkAddress)", expression.ToString()); // cast($it.Location, 'Microsoft.AspNetCore.OData.Tests.Models.WorkAddress') where $it is the Employee entity + Assert.Equal("Microsoft.AspNetCore.OData.Tests.Models.WorkAddress", expression.Type.ToString()); + Assert.Equal("Location", ((expression as UnaryExpression).Operand as MemberExpression).Member.Name); + Assert.Equal(typeof(WorkAddress), expression.Type); + Assert.Equal(ExpressionType.TypeAs, expression.NodeType); + Assert.Equal(workAddressType.FullName(), (expression as UnaryExpression).Type.FullName); + Assert.Equal(addressType.FullName(), (expression as UnaryExpression).Operand.Type.FullName); + } + [Fact] public void BindSingleValueFunctionCallNode_ThrowsArgumentNull_ForInputs() { @@ -170,3 +304,58 @@ public static PropertyInfo Call_GetDynamicPropertyContainer(SingleValueOpenPrope return GetDynamicPropertyContainer(openNode, context); } } + +public static class HardCodedTestModel +{ + #region Create the model + private static readonly IEdmModel Model = BuildAndGetEdmModel(); + + public static IEdmModel TestModel + { + get { return Model; } + } + + public static IEdmEntityType GetEntityType(string entityQualifiedName) + { + return TestModel.FindDeclaredType(entityQualifiedName) as IEdmEntityType; + } + + public static IEdmComplexType GetEdmComplexType(string complexTypeQualifiedName) + { + return TestModel.FindDeclaredType(complexTypeQualifiedName) as IEdmComplexType; + } + + public static IEdmEntityTypeReference GetEntityTypeReference(IEdmEntityType entityType) + { + return new EdmEntityTypeReference(entityType, false); + } + + public static IEdmComplexTypeReference GetComplexTypeReference(IEdmComplexType complexType) + { + // Create a complex type reference using the EdmCoreModel + return new EdmComplexTypeReference(complexType, isNullable: false); + } + + public static IEdmProperty GetEmployeeLocationProperty() + { + return GetEntityType("Microsoft.AspNetCore.OData.Tests.Models.Employee").FindProperty("Location"); + } + + public static IEdmEntitySet GetEmployeeSet() + { + return TestModel.EntityContainer.FindEntitySet("Employee"); + } + + private static IEdmModel BuildAndGetEdmModel() + { + var builder = new ODataConventionModelBuilder(); + builder.Namespace = "Microsoft.AspNetCore.OData.Tests.Models"; + builder.EntitySet("Employees"); + builder.ComplexType
(); + builder.ComplexType(); + builder.EntitySet("Managers"); + + return builder.GetEdmModel(); + } + #endregion +}