Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support type cast in group by #1182

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
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();
}
}
}