Skip to content

Commit

Permalink
Support type cast in group by (#1182)
Browse files Browse the repository at this point in the history
  • Loading branch information
clemvnt authored Feb 6, 2025
1 parent 1510d6d commit 2d16203
Show file tree
Hide file tree
Showing 4 changed files with 246 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.AspNetCore.OData.Edm;
using Microsoft.AspNetCore.OData.Query.Wrapper;
using Microsoft.OData.Edm;
using Microsoft.OData.ModelBuilder;
Expand Down Expand Up @@ -129,6 +130,9 @@ protected Expression BindAccessor(QueryNode node, Expression baseElement = null)
return BindSingleValueFunctionCallNode(node as SingleValueFunctionCallNode);
case QueryNodeKind.Constant:
return BindConstantNode(node as ConstantNode);
case QueryNodeKind.SingleResourceCast:
var singleResourceCastNode = node as SingleResourceCastNode;
return BindSingleResourceCastNode(singleResourceCastNode, baseElement);
default:
throw Error.NotSupported(SRResources.QueryNodeBindingNotSupported, node.Kind,
typeof(AggregationBinder).Name);
Expand Down Expand Up @@ -172,4 +176,15 @@ private Expression CreateOpenPropertyAccessExpression(SingleValueOpenPropertyAcc
nullExpression);
}
}
}

private Expression BindSingleResourceCastNode(SingleResourceCastNode node, Expression baseElement = null)
{
IEdmStructuredTypeReference structured = node.StructuredTypeReference;
Contract.Assert(structured != null, "NS casts can contain only structured types");

Type clrType = Model.GetClrType(structured);

Expression source = Bind(node.Source);
return Expression.TypeAs(source, clrType);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -324,4 +324,44 @@ public static ODataModelBuilder Add_OrderCustomer_Binding(this ODataModelBuilder
builder.EntitySet<Order>("Orders").HasRequiredBinding(o => o.Customer, "Customer");
return builder;
}
}

public static ODataModelBuilder Add_Products_EntityType(this ODataModelBuilder builder)
{
var product = builder.EntityType<Product>();
product.HasKey(p => new { p.ProductID });
product.Property(p => p.ProductID);
product.Property(p => p.ProductName);
product.ComplexProperty(p => p.Category);
return builder;
}

public static ODataModelBuilder Add_DerivedProducts_EntityType(this ODataModelBuilder builder)
{
var derivedProduct = builder.EntityType<DerivedProduct>();
derivedProduct.DerivesFrom<Product>();
derivedProduct.Property(p => p.DerivedProductName);
return builder;
}

public static ODataModelBuilder Add_Categories_EntityType(this ODataModelBuilder builder)
{
var category = builder.ComplexType<Category>();
category.Property(c => c.CategoryID);
category.Property(c => c.CategoryName);
return builder;
}

public static ODataModelBuilder Add_DerivedCategories_EntityType(this ODataModelBuilder builder)
{
var derivedCategory = builder.ComplexType<DerivedCategory>();
derivedCategory.DerivesFrom<Category>();
derivedCategory.Property(c => c.DerivedCategoryName);
return builder;
}

public static ODataModelBuilder Add_Products_EntitySet(this ODataModelBuilder builder)
{
builder.EntitySet<Product>("Products");
return builder;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
using Microsoft.AspNetCore.OData.Abstracts;
using Microsoft.AspNetCore.OData.Query;
using Microsoft.AspNetCore.OData.Query.Expressions;
using Microsoft.AspNetCore.OData.Tests.Commons;
using Microsoft.AspNetCore.OData.Tests.Models;
using Microsoft.OData;
using Microsoft.OData.Edm;
using Microsoft.OData.ModelBuilder;
using Microsoft.OData.UriParser;
Expand Down Expand Up @@ -193,6 +195,46 @@ public void ClassicEFQueryShape()
classicEF: true);
}

[Fact]
public void NSCast_OnSingleEntity_GeneratesExpression_WithAsOperator()
{
var filters = VerifyQueryDeserialization(
"groupby((Microsoft.AspNetCore.OData.Tests.Models.Product/ProductName))",
".GroupBy($it => new GroupByWrapper() {GroupByContainer = new LastInChain() {Name = ProductName, Value = ($it As Product).ProductName, }, })"
+ ".Select($it => new AggregationWrapper() {GroupByContainer = $it.Key.GroupByContainer, })");
}

[Theory]
[InlineData("groupby((Microsoft.AspNetCore.OData.Tests.Models.Product/ProductName))", ".GroupBy($it => new GroupByWrapper() {GroupByContainer = new LastInChain() {Name = ProductName, Value = ($it As Product).ProductName, }, })")]
[InlineData("groupby((Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct/DerivedProductName))", ".GroupBy($it => new GroupByWrapper() {GroupByContainer = new LastInChain() {Name = DerivedProductName, Value = ($it As DerivedProduct).DerivedProductName, }, })")]
[InlineData("groupby((Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct/Category/CategoryName))", ".GroupBy($it => new GroupByWrapper() {GroupByContainer = new NestedPropertyLastInChain() {Name = Category, NestedValue = new GroupByWrapper() {GroupByContainer = new LastInChain() {Name = CategoryName, Value = ($it As DerivedProduct).Category.CategoryName, }, }, }, })")]
[InlineData("groupby((Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct/Category/Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory/DerivedCategoryName))", ".GroupBy($it => new GroupByWrapper() {GroupByContainer = new NestedPropertyLastInChain() {Name = Category, NestedValue = new GroupByWrapper() {GroupByContainer = new LastInChain() {Name = DerivedCategoryName, Value = (($it As DerivedProduct).Category As DerivedCategory).DerivedCategoryName, }, }, }, })")]
public void Inheritance_WithDerivedInstance(string filter, string expectedResult)
{
var filters = VerifyQueryDeserialization<DerivedProduct>(filter, expectedResult
+ ".Select($it => new AggregationWrapper() {GroupByContainer = $it.Key.GroupByContainer, })");
}

[Theory]
[InlineData("groupby((ProductName))", ".GroupBy($it => new GroupByWrapper() {GroupByContainer = new LastInChain() {Name = ProductName, Value = $it.ProductName, }, })")]
[InlineData("groupby((Category/Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory/DerivedCategoryName))", ".GroupBy($it => new GroupByWrapper() {GroupByContainer = new NestedPropertyLastInChain() {Name = Category, NestedValue = new GroupByWrapper() {GroupByContainer = new LastInChain() {Name = DerivedCategoryName, Value = ($it.Category As DerivedCategory).DerivedCategoryName, }, }, }, })")]
[InlineData("groupby((Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct/DerivedProductName))", ".GroupBy($it => new GroupByWrapper() {GroupByContainer = new LastInChain() {Name = DerivedProductName, Value = ($it As DerivedProduct).DerivedProductName, }, })")]
[InlineData("groupby((Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct/Category/CategoryName))", ".GroupBy($it => new GroupByWrapper() {GroupByContainer = new NestedPropertyLastInChain() {Name = Category, NestedValue = new GroupByWrapper() {GroupByContainer = new LastInChain() {Name = CategoryName, Value = ($it As DerivedProduct).Category.CategoryName, }, }, }, })")]
[InlineData("groupby((Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct/Category/Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory/DerivedCategoryName))", ".GroupBy($it => new GroupByWrapper() {GroupByContainer = new NestedPropertyLastInChain() {Name = Category, NestedValue = new GroupByWrapper() {GroupByContainer = new LastInChain() {Name = DerivedCategoryName, Value = (($it As DerivedProduct).Category As DerivedCategory).DerivedCategoryName, }, }, }, })")]
public void Inheritance_WithBaseInstance(string filter, string expectedResult)
{
var filters = VerifyQueryDeserialization<Product>(filter, expectedResult
+ ".Select($it => new AggregationWrapper() {GroupByContainer = $it.Key.GroupByContainer, })");
}

[Fact]
public void CastToNonDerivedType_Throws()
{
ExceptionAssert.Throws<ODataException>(
() => VerifyQueryDeserialization<Product>("groupby((Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory/CategoryName))"),
"Encountered invalid type cast. 'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory' is not assignable from 'Microsoft.AspNetCore.OData.Tests.Models.Product'.");
}

private Expression VerifyQueryDeserialization(string filter, string expectedResult = null, Action<ODataQuerySettings> settingsCustomizer = null, bool classicEF = false)
{
return VerifyQueryDeserialization<Product>(filter, expectedResult, settingsCustomizer, classicEF);
Expand Down Expand Up @@ -288,7 +330,7 @@ private IEdmModel GetModel<T>() where T : class

private class AggregationBinderEFFake : AggregationBinder
{
internal AggregationBinderEFFake(ODataQuerySettings settings, IAssemblyResolver assembliesResolver, Type elementType, IEdmModel model, TransformationNode transformation)
internal AggregationBinderEFFake(ODataQuerySettings settings, IAssemblyResolver assembliesResolver, Type elementType, IEdmModel model, TransformationNode transformation)
: base(settings, assembliesResolver, elementType, model, transformation)
{
}
Expand All @@ -298,4 +340,4 @@ internal override bool IsClassicEF(IQueryable query)
return true;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1600,6 +1600,150 @@ public void SortOnNestedDynamicPropertyWorks()
}
}

public static List<Product> ProductApplyForTypeCastTestData
{
get
{
List<Product> productList = new List<Product>();

Product p1 = new DerivedProduct
{
ProductID = 1,
ProductName = "Product 1",
DerivedProductName = "Product A",
Category = new DerivedCategory
{
CategoryID = 1,
CategoryName = "Category 1",
DerivedCategoryName = "Category A",
},
};
productList.Add(p1);
Product p2 = new DerivedProduct
{
ProductID = 2,
ProductName = "Product 1",
DerivedProductName = "Product A",
Category = new DerivedCategory
{
CategoryID = 1,
CategoryName = "Category 1",
DerivedCategoryName = "Category A",
},
};
productList.Add(p2);
Product p3 = new DerivedProduct
{
ProductID = 3,
ProductName = "Product 2",
DerivedProductName = "Product B",
Category = new DerivedCategory
{
CategoryID = 2,
CategoryName = "Category 2",
DerivedCategoryName = "Category B",
},
};
productList.Add(p3);

return productList;
}
}

public static TheoryDataSet<string, List<Dictionary<string, object>>> ProductTestAppliesForTypeCast
{
get
{
return new TheoryDataSet<string, List<Dictionary<string, object>>>
{
{
"groupby((Microsoft.AspNetCore.OData.Tests.Models.Product/ProductName))",
new List<Dictionary<string, object>>
{
new Dictionary<string, object> { {"ProductName", "Product 1"} },
new Dictionary<string, object> { {"ProductName", "Product 2"} }
}
},
{
"groupby((Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct/DerivedProductName))",
new List<Dictionary<string, object>>
{
new Dictionary<string, object> { { "DerivedProductName", "Product A"} },
new Dictionary<string, object> { { "DerivedProductName", "Product B"} }
}
},
{
"groupby((Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct/Category/CategoryName))",
new List<Dictionary<string, object>>
{
new Dictionary<string, object> { { "Category/CategoryName", "Category 1" } },
new Dictionary<string, object> { { "Category/CategoryName", "Category 2" } }
}
},
{
"groupby((Category/Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory/DerivedCategoryName))",
new List<Dictionary<string, object>>
{
new Dictionary<string, object> { { "Category/DerivedCategoryName", "Category A" } },
new Dictionary<string, object> { { "Category/DerivedCategoryName", "Category B" } }
}
},
{
"groupby((Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct/Category/Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory/DerivedCategoryName))",
new List<Dictionary<string, object>>
{
new Dictionary<string, object> { { "Category/DerivedCategoryName", "Category A" } },
new Dictionary<string, object> { { "Category/DerivedCategoryName", "Category B" } }
}
}
};
}
}

[Theory]
[MemberData(nameof(ProductTestAppliesForTypeCast))]
public void ApplyTo_Returns_Correct_Queryable_ForTypeCast(string apply, List<Dictionary<string, object>> aggregation)
{
// Arrange
var model = new ODataModelBuilder()
.Add_Products_EntityType()
.Add_DerivedProducts_EntityType()
.Add_Categories_EntityType()
.Add_DerivedCategories_EntityType()
.Add_Products_EntitySet()
.GetEdmModel();
var context = new ODataQueryContext(model, typeof(Product)) { RequestContainer = new MockServiceProvider() };
var queryOptionParser = new ODataQueryOptionParser(
context.Model,
context.ElementType,
context.NavigationSource,
new Dictionary<string, string> { { "$apply", apply } });
var applyOption = new ApplyQueryOption(apply, context, queryOptionParser);
IEnumerable<Product> products = ProductApplyForTypeCastTestData;

// Act
IQueryable queryable = applyOption.ApplyTo(products.AsQueryable(), new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.True });

// Assert
Assert.NotNull(queryable);
var actualProducts = Assert.IsAssignableFrom<IEnumerable<DynamicTypeWrapper>>(queryable).ToList();

Assert.Equal(aggregation.Count(), actualProducts.Count());

var aggEnum = actualProducts.GetEnumerator();

foreach (var expected in aggregation)
{
aggEnum.MoveNext();
var agg = aggEnum.Current;
foreach (var key in expected.Keys)
{
object value = GetValue(agg, key);
Assert.Equal(expected[key], value);
}
}
}

private object GetValue(DynamicTypeWrapper wrapper, string path)
{
var parts = path.Split('/');
Expand Down Expand Up @@ -1647,4 +1791,4 @@ public IQueryable<Customer> Get()
{
return _customers.AsQueryable();
}
}
}

0 comments on commit 2d16203

Please sign in to comment.