diff --git a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml index 7c2fed9a4..29e3414ad 100644 --- a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml +++ b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml @@ -7703,6 +7703,11 @@ Looks up a localized string similar to The type '{0}' must be an enum or Nullable<T> where T is an enum type.. + + + Looks up a localized string similar to The type '{0}' must be an open type. The dynamic properties container property is only expected on open types.. + + Looks up a localized string similar to The type '{0}' does not inherit from and is not a base type of '{1}'.. @@ -9401,7 +9406,7 @@ Gets property for dynamic properties dictionary. - + The single-valued open property access node. The query binder context. Returns CLR property for dynamic properties container. @@ -9413,6 +9418,24 @@ The query binder context. Returns null if no aggregations were used so far + + + Binds property to the source node. + + The source node. + The property. + The query binder context. + The LINQ created. + + + + Gets property for dynamic properties dictionary. + + The Edm type reference. + Query node kind. + The Edm model. + Returns CLR property for dynamic properties container. + Apply null propagation for filter body. @@ -9421,6 +9444,40 @@ The query binder context. The LINQ created. + + + Binds dynamic property to the source node. + + The query node to bind. + The query binder context. + The LINQ created. + + + + Creates an expression for retrieving a dynamic property from the dynamic properties container property. + + The dynamic properties container property access expression. + The dynamic property name. + The query binder context. + The LINQ created. + + + + Binds nested dynamic property to the source node. + + The query node to bind. + The query binder context. + The LINQ created. + + + + Creates an expression for retrieving a nested dynamic property. + + The source expression. + The query node to bind. + The query binder context. + The LINQ created. + Binds a to create a LINQ that diff --git a/src/Microsoft.AspNetCore.OData/Properties/SRResources.Designer.cs b/src/Microsoft.AspNetCore.OData/Properties/SRResources.Designer.cs index a3a66ee9e..8602389e2 100644 --- a/src/Microsoft.AspNetCore.OData/Properties/SRResources.Designer.cs +++ b/src/Microsoft.AspNetCore.OData/Properties/SRResources.Designer.cs @@ -1797,6 +1797,15 @@ internal static string TypeMustBeEnumOrNullableEnum { } } + /// + /// Looks up a localized string similar to The type '{0}' must be an open type. The dynamic properties container property is only expected on open types.. + /// + internal static string TypeMustBeOpenType { + get { + return ResourceManager.GetString("TypeMustBeOpenType", resourceCulture); + } + } + /// /// Looks up a localized string similar to The type '{0}' does not inherit from and is not a base type of '{1}'.. /// diff --git a/src/Microsoft.AspNetCore.OData/Properties/SRResources.resx b/src/Microsoft.AspNetCore.OData/Properties/SRResources.resx index 87ee6b31d..e7981a72f 100644 --- a/src/Microsoft.AspNetCore.OData/Properties/SRResources.resx +++ b/src/Microsoft.AspNetCore.OData/Properties/SRResources.resx @@ -745,4 +745,7 @@ Unable to identify a unique property named '{0}'. {0} = Property Name + + The type '{0}' must be an open type. The dynamic properties container property is only expected on open types. + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.cs b/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.cs index 7f2549505..a53057d4a 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.cs @@ -24,6 +24,7 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; using Microsoft.OData.UriParser; +using Microsoft.VisualBasic; namespace Microsoft.AspNetCore.OData.Query.Expressions; @@ -297,30 +298,13 @@ public virtual Expression BindDynamicPropertyAccessQueryNode(SingleValueOpenProp return Bind(computedProperty.Expression, context); } - PropertyInfo prop = GetDynamicPropertyContainer(openNode, context); - - var propertyAccessExpression = BindPropertyAccessExpression(openNode, prop, context); - var readDictionaryIndexerExpression = Expression.Property(propertyAccessExpression, - DictionaryStringObjectIndexerName, Expression.Constant(openNode.Name)); - var containsKeyExpression = Expression.Call(propertyAccessExpression, - propertyAccessExpression.Type.GetMethod("ContainsKey"), Expression.Constant(openNode.Name)); - var nullExpression = Expression.Constant(null); - - if (context.QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True) + if (openNode.Source is SingleValueOpenPropertyAccessNode) { - var dynamicDictIsNotNull = Expression.NotEqual(propertyAccessExpression, Expression.Constant(null)); - var dynamicDictIsNotNullAndContainsKey = Expression.AndAlso(dynamicDictIsNotNull, containsKeyExpression); - return Expression.Condition( - dynamicDictIsNotNullAndContainsKey, - readDictionaryIndexerExpression, - nullExpression); + return BindNestedDynamicPropertyAccessExpression(openNode, context); } else { - return Expression.Condition( - containsKeyExpression, - readDictionaryIndexerExpression, - nullExpression); + return BindDynamicPropertyAccessExpression(openNode, context); } } @@ -978,7 +962,7 @@ protected Expression[] BindArguments(IEnumerable nodes, QueryBinderCo /// /// Gets property for dynamic properties dictionary. /// - /// + /// The single-valued open property access node. /// The query binder context. /// Returns CLR property for dynamic properties container. protected static PropertyInfo GetDynamicPropertyContainer(SingleValueOpenPropertyAccessNode openNode, QueryBinderContext context) @@ -993,22 +977,7 @@ protected static PropertyInfo GetDynamicPropertyContainer(SingleValueOpenPropert throw Error.ArgumentNull(nameof(context)); } - IEdmStructuredType edmStructuredType; - IEdmTypeReference edmTypeReference = openNode.Source.TypeReference; - if (edmTypeReference.IsEntity()) - { - edmStructuredType = edmTypeReference.AsEntity().EntityDefinition(); - } - else if (edmTypeReference.IsComplex()) - { - edmStructuredType = edmTypeReference.AsComplex().ComplexDefinition(); - } - else - { - throw Error.NotSupported(SRResources.QueryNodeBindingNotSupported, openNode.Kind, typeof(QueryBinder).Name); - } - - return context.Model.GetDynamicPropertyDictionary(edmStructuredType); + return GetDynamicPropertyContainer(openNode.Source.TypeReference, openNode.Kind, context.Model); } /// @@ -1322,6 +1291,11 @@ internal Type RetrieveClrTypeForConstant(IEdmTypeReference edmTypeReference, Que constantType = Nullable.GetUnderlyingType(constantType) ?? constantType; } + if (edmTypeReference != null && edmTypeReference.IsUntyped()) + { + constantType = typeof(object); + } + return constantType; } @@ -1409,10 +1383,19 @@ private static Expression All(Expression source, Expression filter) } } - private Expression BindPropertyAccessExpression(SingleValueOpenPropertyAccessNode openNode, PropertyInfo prop, QueryBinderContext context) + /// + /// Binds property to the source node. + /// + /// The source node. + /// The property. + /// The query binder context. + /// The LINQ created. + private Expression BindPropertyAccessExpression(SingleValueNode sourceNode, PropertyInfo prop, QueryBinderContext context) { - var source = Bind(openNode.Source, context); + Expression source = Bind(sourceNode, context); + Expression propertyAccessExpression; + if (context.QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True && ExpressionBinderHelper.IsNullable(source.Type) && source != context.CurrentParameter) { @@ -1422,9 +1405,43 @@ private Expression BindPropertyAccessExpression(SingleValueOpenPropertyAccessNod { propertyAccessExpression = Expression.Property(source, prop.Name); } + return propertyAccessExpression; } + /// + /// Gets property for dynamic properties dictionary. + /// + /// The Edm type reference. + /// Query node kind. + /// The Edm model. + /// Returns CLR property for dynamic properties container. + private static PropertyInfo GetDynamicPropertyContainer(IEdmTypeReference edmTypeReference, QueryNodeKind queryNodeKind, IEdmModel model) + { + Debug.Assert(edmTypeReference != null, $"{nameof(edmTypeReference) != null}"); + + IEdmStructuredType edmStructuredType; + if (edmTypeReference.IsEntity()) + { + edmStructuredType = edmTypeReference.AsEntity().EntityDefinition(); + } + else if (edmTypeReference.IsComplex()) + { + edmStructuredType = edmTypeReference.AsComplex().ComplexDefinition(); + } + else + { + throw Error.NotSupported(SRResources.QueryNodeBindingNotSupported, queryNodeKind, typeof(QueryBinder).Name); + } + + if (!edmStructuredType.IsOpen) + { + throw Error.NotSupported(SRResources.TypeMustBeOpenType, edmStructuredType.FullTypeName()); + } + + return model.GetDynamicPropertyDictionary(edmStructuredType); + } + /// /// Apply null propagation for filter body. /// @@ -1453,6 +1470,201 @@ protected static Expression ApplyNullPropagationForFilterBody(Expression body, Q return body; } + /// + /// Binds dynamic property to the source node. + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + private Expression BindDynamicPropertyAccessExpression(SingleValueOpenPropertyAccessNode openNode, QueryBinderContext context) + { + PropertyInfo prop = GetDynamicPropertyContainer(openNode, context); + + Expression containerPropertyAccessExpression = BindPropertyAccessExpression(openNode.Source, prop, context); + + return CreateDynamicPropertyAccessExpression(containerPropertyAccessExpression, openNode.Name, context); + } + + /// + /// Creates an expression for retrieving a dynamic property from the dynamic properties container property. + /// + /// The dynamic properties container property access expression. + /// The dynamic property name. + /// The query binder context. + /// The LINQ created. + private static Expression CreateDynamicPropertyAccessExpression( + Expression dynamicPropertiesContainerExpr, + string propertyName, + QueryBinderContext context) + { + // Get dynamic property value + IndexExpression dictionaryIndexExpr = Expression.Property( + dynamicPropertiesContainerExpr, DictionaryStringObjectIndexerName, + Expression.Constant(propertyName)); + + // ContainsKey method + MethodCallExpression containsKeyExpression = Expression.Call( + dynamicPropertiesContainerExpr, + dynamicPropertiesContainerExpr.Type.GetMethod("ContainsKey"), + Expression.Constant(propertyName)); + + // Handle null propagation + if (context.QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True) + { + BinaryExpression dictIonaryIsNotNullExpr = Expression.NotEqual(dynamicPropertiesContainerExpr, Expression.Constant(null)); + BinaryExpression dictIonaryIsNotNullAndContainsKeyExpr = Expression.AndAlso(dictIonaryIsNotNullExpr, containsKeyExpression); + return Expression.Condition( + dictIonaryIsNotNullAndContainsKeyExpr, + dictionaryIndexExpr, + Expression.Constant(null)); + } + else + { + return Expression.Condition( + containsKeyExpression, + dictionaryIndexExpr, + Expression.Constant(null)); + } + } + + /// + /// Binds nested dynamic property to the source node. + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + private Expression BindNestedDynamicPropertyAccessExpression(SingleValueOpenPropertyAccessNode openNode, QueryBinderContext context) + { + // NOTE: Every segment after a dynamic property segment will be represented + // as a dynamic segment though it may not strictly resolve a dynamic property. + // The URI parser is unable to determine that so we handle that here. + + // For the expression $filter=DynamicProperty/Property eq '{value}', + // the Property segment will be a SingleValueOpenPropertyAccessNode but + // can resolve into either a declared or dynamic property + // Similarly, for %filter=DynamicProperty/Level1Property/Level2Property, + // both Level1Property and Level2Property will be SingleValueOpenPropertyAccessNode but + // can resolve into either declared or dynamic properties + + Expression sourceExpr = null; + + if (openNode.Source is SingleValueOpenPropertyAccessNode openNodeParent) + { + sourceExpr = BindNestedDynamicPropertyAccessExpression(openNodeParent, context); + } + else + { + return BindDynamicPropertyAccessExpression(openNode, context); + } + + return CreateNestedDynamicPropertyAccessExpression(sourceExpr, openNode, context); + } + + /// + /// Creates an expression for retrieving a nested dynamic property. + /// + /// The source expression. + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + private static Expression CreateNestedDynamicPropertyAccessExpression( + Expression sourceExpr, + SingleValueOpenPropertyAccessNode openNode, + QueryBinderContext context) + { + // Scenario 1: Declared property => DynamicProperty/DeclaredProperty + // Yields the equivalent of: + // dynamicPropertyValue.GetType().GetProperty(openNode.Name).GetValue(dynamicPropertyValue) + Expression getTypeExpr = Expression.Call(sourceExpr, "GetType", Type.EmptyTypes); + + // Get declared property + Expression getPropertyExpr = Expression.Call( + getTypeExpr, + typeof(Type).GetMethod("GetProperty", new[] { typeof(string) }), + Expression.Constant(openNode.Name)); + + // Get declared property value + Expression getDeclaredPropertyValueExpr = Expression.Call( + getPropertyExpr, + "GetValue", + Type.EmptyTypes, + sourceExpr); + + // SCENARIO 2: Dynamic property => DynamicProperty/DynamicProperty + // Yields the equivalent of: + // dynamicPropertyValue.GetType().GetProperty({DynamicContainerPropertyName}).GetValue(dynamicPropertyValue).Item[openNode.Name] + // GetEdmTypeReference method + MethodInfo getEdmTypeReferenceMethod = typeof(EdmClrTypeMapExtensions).GetMethod( + nameof(EdmClrTypeMapExtensions.GetEdmTypeReference), + BindingFlags.Public | BindingFlags.Static, + new[] { typeof(IEdmModel), typeof(Type) }); + + // Get Edm type reference of the property value type + Expression getEdmTypeReferenceExpr = Expression.Call( + null, + getEdmTypeReferenceMethod, + new Expression[] + { + Expression.Constant(context.Model, typeof(IEdmModel)), + getTypeExpr + }); + + // Throw exception if GetEdmTypeReference returns null - Resource type not in model + Expression formatExceptionExpr = Expression.Call( + typeof(string).GetMethod("Format", new[] { typeof(string), typeof(object) }), + Expression.Constant(SRResources.ResourceTypeNotInModel), + Expression.Property(getTypeExpr, nameof(Type.FullName))); + Expression newExceptionExpr = Expression.New( + typeof(InvalidOperationException).GetConstructor(new[] { typeof(string) }), + formatExceptionExpr); + Expression throwExpresionExpr = Expression.Throw(newExceptionExpr, typeof(object)); + + // GetDynamicPropertyContainer method + MethodInfo getDynamicPropertyContainerMethod = typeof(QueryBinder).GetMethod( + nameof(GetDynamicPropertyContainer), + BindingFlags.NonPublic | BindingFlags.Static, + new[] { typeof(IEdmTypeReference), typeof(QueryNodeKind), typeof(IEdmModel) }); + + // Get dynamic properties container property + Expression getDynamicPropertyContainerExpr = Expression.Call( + null, + getDynamicPropertyContainerMethod, + getEdmTypeReferenceExpr, + Expression.Constant(openNode.Kind, typeof(QueryNodeKind)), + Expression.Constant(context.Model, typeof(IEdmModel))); + + // Get dynamic properties container property value - value is a dictionary + Expression dynamicPropertiesContainerExpr = Expression.Convert( + Expression.Call(getDynamicPropertyContainerExpr, "GetValue", Type.EmptyTypes, sourceExpr), + typeof(IDictionary)); + + // Get dynamic property from container property + Expression getDynamicPropertyValueExpr = CreateDynamicPropertyAccessExpression( + dynamicPropertiesContainerExpr, + openNode.Name, + context); + + // We only consider scenario 2 if scenario 1 doesn't apply + Expression getValueExpr = Expression.Condition( + Expression.NotEqual(getPropertyExpr, Expression.Constant(null, typeof(PropertyInfo))), + getDeclaredPropertyValueExpr, + Expression.Condition( // Throw exception if resource type not in model + Expression.Equal(getEdmTypeReferenceExpr, Expression.Constant(null, typeof(object))), + throwExpresionExpr, + getDynamicPropertyValueExpr)); + + // Handle null propagation + if (context.QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True) + { + return Expression.Condition( + Expression.Equal(sourceExpr, Expression.Constant(null, typeof(object))), + Expression.Constant(null, typeof(object)), + getValueExpr); + } + + return getValueExpr; + } + [DebuggerStepThrough] private static void CheckArgumentNull(T node, QueryBinderContext context) where T : QueryNode { diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterController.cs index 529c9f079..b5a68915e 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterController.cs @@ -20,3 +20,21 @@ public ActionResult> Get() return Ok(DollarFilterDataSource.People); } } + +public class VendorsController : ODataController +{ + [EnableQuery] + public ActionResult> Get() + { + return DollarFilterDataSource.Vendors; + } +} + +public class BadVendorsController : ODataController +{ + [EnableQuery] + public ActionResult> Get() + { + return DollarFilterDataSource.BadVendors; + } +} diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterDataModel.cs index 3e1a5a2b8..1fab6128f 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterDataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterDataModel.cs @@ -5,6 +5,8 @@ // //------------------------------------------------------------------------------ +using System.Collections.Generic; + namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarFilter; public class Person @@ -12,3 +14,34 @@ public class Person public int Id { get; set; } public string SSN { get; set; } } + +public class Vendor +{ + public int Id { get; set; } + public VendorAddress DeclaredSingleValuedProperty { get; set; } + public int DeclaredPrimitiveProperty { get; set; } + public Dictionary DynamicProperties { get; set; } +} + +public class VendorAddress +{ + public string Street { get; set; } + public VendorCity City { get; set; } + public Dictionary DynamicProperties { get; set; } +} + +public class VendorCity +{ + public string Name { get; set; } + public Dictionary DynamicProperties { get; set; } +} + +public class NonOpenVendorAddress +{ + public string Street { get; set; } +} + +public class NotInModelVendorAddress +{ + public string Street { get; set; } +} diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterDataSource.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterDataSource.cs index 2b138d6ae..ce86ab630 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterDataSource.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterDataSource.cs @@ -12,6 +12,8 @@ namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarFilter; public class DollarFilterDataSource { private static IList people; + private static List vendors; + private static List badVendors; static DollarFilterDataSource() { @@ -22,7 +24,191 @@ static DollarFilterDataSource() new Person { Id = 3, SSN = "xyz'" }, new Person { Id = 4, SSN = "'pqr'" } }; + + #region Vendors + + vendors = new List + { + new Vendor + { + Id = 1, + DeclaredPrimitiveProperty = 19, + DeclaredSingleValuedProperty = new VendorAddress + { + Street = "Bourbon Street", + City = new VendorCity + { + Name = "New Orleans", + DynamicProperties = new Dictionary + { + { "State", "Louisiana" } + } + }, + DynamicProperties = new Dictionary + { + { "ZipCode", "25810" } + } + }, + DynamicProperties = new Dictionary + { + { "DynamicPrimitiveProperty", 19 }, + { + "DynamicSingleValuedProperty", + new VendorAddress + { + Street = "Bourbon Street", + City = new VendorCity + { + Name = "New Orleans", + DynamicProperties = new Dictionary + { + { "State", "Louisiana" } + } + }, + DynamicProperties = new Dictionary + { + { "ZipCode", "25810" } + } + } + } + } + }, + new Vendor + { + Id = 2, + DeclaredPrimitiveProperty = 13, + DeclaredSingleValuedProperty = new VendorAddress + { + Street = "Ocean Drive", + City = new VendorCity + { + Name = "Miami", + DynamicProperties = new Dictionary + { + { "State", "Florida" } + } + }, + DynamicProperties = new Dictionary + { + { "ZipCode", "73857" } + } + }, + DynamicProperties = new Dictionary + { + { "DynamicPrimitiveProperty", 13 }, + { + "DynamicSingleValuedProperty", + new VendorAddress + { + Street = "Ocean Drive", + City = new VendorCity + { + Name = "Miami", + DynamicProperties = new Dictionary + { + { "State", "Florida" } + } + }, + DynamicProperties = new Dictionary + { + { "ZipCode", "73857" } + } + } + } + } + }, + new Vendor + { + Id = 3, + DeclaredPrimitiveProperty = 17, + DeclaredSingleValuedProperty = new VendorAddress + { + Street = "Canal Street", + City = new VendorCity + { + Name = "New Orleans", + DynamicProperties = new Dictionary + { + { "State", "Louisiana" } + } + }, + DynamicProperties = new Dictionary + { + { "ZipCode", "11065" } + } + }, + DynamicProperties = new Dictionary + { + { "DynamicPrimitiveProperty", 17 }, + { + "DynamicSingleValuedProperty", + new VendorAddress + { + Street = "Canal Street", + City = new VendorCity + { + Name = "New Orleans", + DynamicProperties = new Dictionary + { + { "State", "Louisiana" } + } + }, + DynamicProperties = new Dictionary + { + { "ZipCode", "11065" } + } + } + } } + } + }; + + #endregion Vendors + + #region Bad Vendors + + badVendors = new List + { + new Vendor + { + Id = 1, + DynamicProperties = new Dictionary + { + { + "WarehouseAddress", + new NonOpenVendorAddress + { + Street = "Madero Street" + } + }, + { + "Foo", + "Bar" + }, + { + "NotInModelAddress", + new NotInModelVendorAddress + { + Street = "No Way" + } + }, + { + "ContainerPropertyNullAddress", + new VendorAddress + { + Street = "Genova Street", + DynamicProperties = null + } + } + } + } + }; + + #endregion Bad Vendors } public static IList People => people; + + public static List Vendors => vendors; + + public static List BadVendors => badVendors; } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterEdmModel.cs index b161ba3dd..0a5f07193 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterEdmModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterEdmModel.cs @@ -16,6 +16,11 @@ public static IEdmModel GetEdmModel() { var builder = new ODataConventionModelBuilder(); builder.EntitySet("People"); + builder.ComplexType(); + builder.ComplexType(); + builder.ComplexType(); + builder.EntitySet("Vendors"); + builder.EntitySet("BadVendors"); return builder.GetEdmModel(); } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterTests.cs index 0db808e9d..f8e7444d7 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterTests.cs @@ -9,9 +9,12 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; +using Microsoft.AspNetCore.OData.Query.Expressions; using Microsoft.AspNetCore.OData.TestCommon; using Microsoft.Extensions.DependencyInjection; using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; +using Microsoft.VisualBasic; using Xunit; namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarFilter; @@ -27,7 +30,10 @@ protected static void UpdateConfigureServices(IServiceCollection services) { IEdmModel model = DollarFilterEdmModel.GetEdmModel(); - services.ConfigureControllers(typeof(PeopleController)); + services.ConfigureControllers( + typeof(PeopleController), + typeof(VendorsController), + typeof(BadVendorsController)); services.AddControllers().AddOData(opt => opt.Filter().Select().AddRouteComponents("odata", model)); @@ -94,4 +100,202 @@ public async Task TestSingleQuotesOnInExpression(string inExpr, string partialRe Assert.EndsWith($"$metadata#People\",\"value\":{partialResult}}}", result); } + + [Theory] + [InlineData("Id eq 2")] + [InlineData("DeclaredPrimitiveProperty eq 13")] + [InlineData("DeclaredSingleValuedProperty/Street eq 'Ocean Drive'")] + [InlineData("DeclaredSingleValuedProperty/ZipCode eq '73857'")] + [InlineData("DeclaredSingleValuedProperty/City/Name eq 'Miami'")] + [InlineData("DeclaredSingleValuedProperty/City/State eq 'Florida'")] + [InlineData("DynamicPrimitiveProperty eq 13")] + [InlineData("DynamicSingleValuedProperty/Street eq 'Ocean Drive'")] + [InlineData("DynamicSingleValuedProperty/ZipCode eq '73857'")] + [InlineData("DynamicSingleValuedProperty/City/Name eq 'Miami'")] + [InlineData("DynamicSingleValuedProperty/City/State eq 'Florida'")] + public async Task TestDeclaredAndDynamicPropertiesInFilterExpressions(string filterExpr) + { + // Arrange + var queryUrl = $"odata/Vendors?$filter={filterExpr}"; + var request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=minimal")); + var client = CreateClient(); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + var result = await response.Content.ReadAsStringAsync(); + + Assert.EndsWith( + "$metadata#Vendors\"," + + "\"value\":[" + + "{\"Id\":2," + + "\"DeclaredPrimitiveProperty\":13," + + "\"DynamicPrimitiveProperty\":13," + + "\"DeclaredSingleValuedProperty\":{" + + "\"Street\":\"Ocean Drive\"," + + "\"ZipCode\":\"73857\",\"City\":{\"Name\":\"Miami\",\"State\":\"Florida\"}}," + + "\"DynamicSingleValuedProperty\":{" + + "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.DollarFilter.VendorAddress\"," + + "\"Street\":\"Ocean Drive\"," + + "\"ZipCode\":\"73857\"," + + "\"City\":{\"Name\":\"Miami\",\"State\":\"Florida\"}}}]}", + result); + } + + [Theory] + [InlineData("Id in (1,3)")] + [InlineData("DeclaredPrimitiveProperty in (19,17)")] + [InlineData("DeclaredSingleValuedProperty/Street in ('Bourbon Street','Canal Street')")] + [InlineData("DeclaredSingleValuedProperty/ZipCode in ('25810','11065')")] + [InlineData("DeclaredSingleValuedProperty/City/Name eq 'New Orleans'")] + [InlineData("DeclaredSingleValuedProperty/City/State eq 'Louisiana'")] + [InlineData("DynamicPrimitiveProperty eq 19 or DynamicPrimitiveProperty eq 17")] + [InlineData("DynamicSingleValuedProperty/Street in ('Bourbon Street','Canal Street')")] + [InlineData("DynamicSingleValuedProperty/ZipCode in ('25810','11065')")] + [InlineData("DynamicSingleValuedProperty/City/Name eq 'New Orleans'")] + [InlineData("DynamicSingleValuedProperty/City/State eq 'Louisiana'")] + public async Task TestDeclaredAndDynamicPropertiesInFilterExpressionsReturningMultipleResources(string filterExpr) + { + // Arrange + var queryUrl = $"odata/Vendors?$filter={filterExpr}"; + var request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=minimal")); + var client = CreateClient(); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + var result = await response.Content.ReadAsStringAsync(); + + Assert.EndsWith( + "$metadata#Vendors\"," + + "\"value\":[" + + "{\"Id\":1," + + "\"DeclaredPrimitiveProperty\":19," + + "\"DynamicPrimitiveProperty\":19," + + "\"DeclaredSingleValuedProperty\":{" + + "\"Street\":\"Bourbon Street\"," + + "\"ZipCode\":\"25810\"," + + "\"City\":{\"Name\":\"New Orleans\",\"State\":\"Louisiana\"}}," + + "\"DynamicSingleValuedProperty\":{" + + "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.DollarFilter.VendorAddress\"," + + "\"Street\":\"Bourbon Street\"," + + "\"ZipCode\":\"25810\"," + + "\"City\":{\"Name\":\"New Orleans\",\"State\":\"Louisiana\"}}}," + + "{\"Id\":3," + + "\"DeclaredPrimitiveProperty\":17," + + "\"DynamicPrimitiveProperty\":17," + + "\"DeclaredSingleValuedProperty\":{" + + "\"Street\":\"Canal Street\"," + + "\"ZipCode\":\"11065\"," + + "\"City\":{\"Name\":\"New Orleans\",\"State\":\"Louisiana\"}}," + + "\"DynamicSingleValuedProperty\":{" + + "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.DollarFilter.VendorAddress\"," + + "\"Street\":\"Canal Street\"," + + "\"ZipCode\":\"11065\"," + + "\"City\":{\"Name\":\"New Orleans\",\"State\":\"Louisiana\"}}}]}", + result); + } + + [Fact] + public async Task TestDynamicPropertySegmentAfterNonOpenDynamicProperty() + { + // Arrange + var queryUrl = $"odata/BadVendors?$filter=WarehouseAddress/City eq 'Mexico City'"; + var request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=minimal")); + var client = CreateClient(); + + // Act + var exception = await Assert.ThrowsAsync(async () => + { + await client.SendAsync(request); + }); + + Assert.NotNull(exception); + var odataException = exception.InnerException?.InnerException; + Assert.NotNull(odataException); + Assert.Equal(string.Format(SRResources.TypeMustBeOpenType, typeof(NonOpenVendorAddress).FullName), + odataException.Message); + } + + [Fact] + public async Task TestDynamicPropertySegmentAfterPrimitiveDynamicProperty() + { + // Arrange + var queryUrl = $"odata/BadVendors?$filter=Foo/City eq 'Mexico City'"; + var request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=minimal")); + var client = CreateClient(); + + // Act + var exception = await Assert.ThrowsAsync(async () => + { + await client.SendAsync(request); + }); + + Assert.NotNull(exception); + var odataException = exception.InnerException?.InnerException; + Assert.NotNull(odataException); + Assert.Equal(string.Format(SRResources.QueryNodeBindingNotSupported, QueryNodeKind.SingleValueOpenPropertyAccess, typeof(QueryBinder).Name), + odataException.Message); + } + + [Fact] + public async Task TestDynamicPropertySegmentOnResourceTypeNotInEdmModel() + { + // Arrange + var queryUrl = $"odata/BadVendors?$filter=NotInModelAddress/City eq 'Mexico City'"; + var request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=minimal")); + var client = CreateClient(); + + // Act + var exception = await Assert.ThrowsAsync(async () => + { + await client.SendAsync(request); + }); + + Assert.NotNull(exception); + var odataException = exception.InnerException?.InnerException; + Assert.NotNull(odataException); + Assert.Equal(string.Format(SRResources.ResourceTypeNotInModel, typeof(NotInModelVendorAddress).FullName), + odataException.Message); + } + + [Theory] + [InlineData("ContainerPropertyNullAddress/City eq 'Mexico City'")] + [InlineData("NonExistentDynamicProperty eq 404")] + public async Task TestNullPropagationForNullDynamicContainerProperty(string filterExpr) + { + // Arrange + var queryUrl = $"odata/BadVendors?$filter={filterExpr}"; + var request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=minimal")); + var client = CreateClient(); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + var result = await response.Content.ReadAsStringAsync(); + Assert.Equal( + "{\"@odata.context\":\"http://localhost/odata/$metadata#BadVendors\",\"value\":[]}", + result); + } }