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);
+ }
}