Skip to content

Commit 85d3d3c

Browse files
committed
Add IFlatteningBinder that aggregation binder can optionally implement
1 parent 4ab4797 commit 85d3d3c

14 files changed

+418
-95
lines changed

src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml

+48-37
Original file line numberDiff line numberDiff line change
@@ -9395,42 +9395,6 @@
93959395
an <see cref="T:System.Linq.Expressions.Expression"/>.
93969396
</summary>
93979397
</member>
9398-
<member name="M:Microsoft.AspNetCore.OData.Query.Expressions.IAggregationBinder.FlattenReferencedProperties(Microsoft.OData.UriParser.Aggregation.TransformationNode,System.Linq.IQueryable,Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext)">
9399-
<summary>
9400-
Flattens properties referenced in aggregate clause to avoid generation of nested queries by Entity Framework.
9401-
</summary>
9402-
<param name="transformationNode">The OData $apply parse tree represented by <see cref="T:Microsoft.OData.UriParser.Aggregation.TransformationNode"/>.</param>
9403-
<param name="query">The original <see cref="T:System.Linq.IQueryable"/>.</param>
9404-
<param name="context">An instance of the <see cref="T:Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext"/> containing the current query context.</param>
9405-
<returns>
9406-
An <see cref="T:Microsoft.AspNetCore.OData.Query.Expressions.AggregationFlatteningResult"/> containing the modified query source and
9407-
additional metadata resulting from the flattening operation.
9408-
</returns>
9409-
<remarks>
9410-
This method generates a Select expression that flattens the properties referenced in the aggregate clause.
9411-
Flattening properties helps prevent the generation of nested queries by Entity Framework,
9412-
resulting in more efficient SQL generation.
9413-
For query like:
9414-
<code>
9415-
groupby((A),aggregate(B/C with max as Alias1,B/D with max as Alias2))
9416-
</code>
9417-
generate an expression similar to:
9418-
<code>
9419-
$it => new FlatteningWrapper() {
9420-
Source = $it,
9421-
Container = new {
9422-
Value = $it.B.C
9423-
Next = new {
9424-
Value = $it.B.D
9425-
}
9426-
}
9427-
}
9428-
</code>
9429-
Also populate expressions to access B/C and B/D in aggregate stage to look like:
9430-
B/C : $it.Container.Value
9431-
B/D : $it.Container.Next.Value
9432-
</remarks>
9433-
</member>
94349398
<member name="M:Microsoft.AspNetCore.OData.Query.Expressions.IAggregationBinder.BindGroupBy(Microsoft.OData.UriParser.Aggregation.TransformationNode,Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext)">
94359399
<summary>
94369400
Translates an OData $apply parse tree represented by a <see cref="T:Microsoft.OData.UriParser.Aggregation.TransformationNode"/> to
@@ -9529,6 +9493,53 @@
95299493
<returns>The filter binder result.</returns>
95309494
<remarks>reconsider to return "LambdaExpression"? </remarks>
95319495
</member>
9496+
<member name="T:Microsoft.AspNetCore.OData.Query.Expressions.IFlatteningBinder">
9497+
<summary>
9498+
Provides an abstraction for flattening property access expressions within an OData $apply clause
9499+
to support efficient translation of aggregation pipelines in LINQ providers like Entity Framework.
9500+
</summary>
9501+
<remarks>
9502+
Entity Framework versions earlier than EF Core 6.0 may generate nested queries when accessing navigation properties
9503+
in aggregation clauses. Flattening these properties can help generate flatter, more efficient SQL queries.
9504+
This interface allows conditional support for flattening based on the capabilities of the underlying LINQ provider.
9505+
</remarks>
9506+
</member>
9507+
<member name="M:Microsoft.AspNetCore.OData.Query.Expressions.IFlatteningBinder.FlattenReferencedProperties(Microsoft.OData.UriParser.Aggregation.TransformationNode,System.Linq.IQueryable,Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext)">
9508+
<summary>
9509+
Flattens properties referenced in aggregate clause to avoid generation of nested queries by Entity Framework.
9510+
</summary>
9511+
<param name="transformationNode">The OData $apply parse tree represented by <see cref="T:Microsoft.OData.UriParser.Aggregation.TransformationNode"/>.</param>
9512+
<param name="query">The original <see cref="T:System.Linq.IQueryable"/>.</param>
9513+
<param name="context">An instance of the <see cref="T:Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext"/> containing the current query context.</param>
9514+
<returns>
9515+
An <see cref="T:Microsoft.AspNetCore.OData.Query.Expressions.AggregationFlatteningResult"/> containing the modified query source and
9516+
additional metadata resulting from the flattening operation.
9517+
</returns>
9518+
<remarks>
9519+
This method generates a Select expression that flattens the properties referenced in the aggregate clause.
9520+
Flattening properties helps prevent the generation of nested queries by Entity Framework,
9521+
resulting in more efficient SQL generation.
9522+
For query like:
9523+
<code>
9524+
groupby((A),aggregate(B/C with max as Alias1,B/D with max as Alias2))
9525+
</code>
9526+
generate an expression similar to:
9527+
<code>
9528+
$it => new FlatteningWrapper() {
9529+
Source = $it,
9530+
Container = new {
9531+
Value = $it.B.C
9532+
Next = new {
9533+
Value = $it.B.D
9534+
}
9535+
}
9536+
}
9537+
</code>
9538+
Also populate expressions to access B/C and B/D in aggregate stage to look like:
9539+
B/C : $it.Container.Value
9540+
B/D : $it.Container.Next.Value
9541+
</remarks>
9542+
</member>
95329543
<member name="T:Microsoft.AspNetCore.OData.Query.Expressions.IOrderByBinder">
95339544
<summary>
95349545
Exposes the ability to translate an OData $orderby represented by <see cref="T:Microsoft.OData.UriParser.OrderByClause"/> to the <see cref="T:System.Linq.Expressions.Expression"/>
@@ -13318,7 +13329,7 @@
1331813329
Validates that the provided type implements <see cref="T:Microsoft.AspNetCore.OData.Query.Wrapper.IGroupByWrapper`2"/> and <see cref="T:Microsoft.AspNetCore.OData.Query.Wrapper.IFlatteningWrapper`1"/>, and inherits from <see cref="T:Microsoft.AspNetCore.OData.Query.Wrapper.DynamicTypeWrapper"/>.
1331913330
</summary>
1332013331
<param name="flattenedExpressionType">The type representing the flattened expression returned by
13321-
<see cref="M:Microsoft.AspNetCore.OData.Query.Expressions.IAggregationBinder.FlattenReferencedProperties(Microsoft.OData.UriParser.Aggregation.TransformationNode,System.Linq.IQueryable,Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext)"/>.</param>
13332+
<see cref="M:Microsoft.AspNetCore.OData.Query.Expressions.IFlatteningBinder.FlattenReferencedProperties(Microsoft.OData.UriParser.Aggregation.TransformationNode,System.Linq.IQueryable,Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext)"/>.</param>
1332213333
<exception cref="T:System.InvalidOperationException">Thrown if <paramref name="flattenedExpressionType"/>
1332313334
does not implement the required interfaces or inherit from the required base class.</exception>
1332413335
</member>

src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt

+2-1
Original file line numberDiff line numberDiff line change
@@ -917,11 +917,12 @@ Microsoft.AspNetCore.OData.Query.Expressions.FilterBinder.FilterBinder() -> void
917917
Microsoft.AspNetCore.OData.Query.Expressions.IAggregationBinder
918918
Microsoft.AspNetCore.OData.Query.Expressions.IAggregationBinder.BindGroupBy(Microsoft.OData.UriParser.Aggregation.TransformationNode transformationNode, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) -> System.Linq.Expressions.Expression
919919
Microsoft.AspNetCore.OData.Query.Expressions.IAggregationBinder.BindSelect(Microsoft.OData.UriParser.Aggregation.TransformationNode transformationNode, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) -> System.Linq.Expressions.Expression
920-
Microsoft.AspNetCore.OData.Query.Expressions.IAggregationBinder.FlattenReferencedProperties(Microsoft.OData.UriParser.Aggregation.TransformationNode transformationNode, System.Linq.IQueryable query, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) -> Microsoft.AspNetCore.OData.Query.Expressions.AggregationFlatteningResult
921920
Microsoft.AspNetCore.OData.Query.Expressions.IComputeBinder
922921
Microsoft.AspNetCore.OData.Query.Expressions.IComputeBinder.BindCompute(Microsoft.OData.UriParser.Aggregation.ComputeTransformationNode computeTransformationNode, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) -> System.Linq.Expressions.Expression
923922
Microsoft.AspNetCore.OData.Query.Expressions.IFilterBinder
924923
Microsoft.AspNetCore.OData.Query.Expressions.IFilterBinder.BindFilter(Microsoft.OData.UriParser.FilterClause filterClause, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) -> System.Linq.Expressions.Expression
924+
Microsoft.AspNetCore.OData.Query.Expressions.IFlatteningBinder
925+
Microsoft.AspNetCore.OData.Query.Expressions.IFlatteningBinder.FlattenReferencedProperties(Microsoft.OData.UriParser.Aggregation.TransformationNode transformationNode, System.Linq.IQueryable query, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) -> Microsoft.AspNetCore.OData.Query.Expressions.AggregationFlatteningResult
925926
Microsoft.AspNetCore.OData.Query.Expressions.IOrderByBinder
926927
Microsoft.AspNetCore.OData.Query.Expressions.IOrderByBinder.BindOrderBy(Microsoft.OData.UriParser.OrderByClause orderByClause, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) -> Microsoft.AspNetCore.OData.Query.Expressions.OrderByBinderResult
927928
Microsoft.AspNetCore.OData.Query.Expressions.ISearchBinder

src/Microsoft.AspNetCore.OData/Query/Expressions/AggregationBinder.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.OData.Query.Expressions
2626
/// <summary>
2727
/// The default implementation to bind an OData $apply represented by <see cref="ApplyClause"/> to an <see cref="Expression"/>.
2828
/// </summary>
29-
public class AggregationBinder : QueryBinder, IAggregationBinder
29+
public class AggregationBinder : QueryBinder, IAggregationBinder, IFlatteningBinder
3030
{
3131
/// <inheritdoc/>
3232
public virtual Expression BindGroupBy(TransformationNode transformationNode, QueryBinderContext context)

src/Microsoft.AspNetCore.OData/Query/Expressions/BinderExtensions.cs

+6-5
Original file line numberDiff line numberDiff line change
@@ -402,12 +402,13 @@ public static IQueryable ApplyBind(this IAggregationBinder binder, IQueryable so
402402
// In this case, the first groupby will be applied to the original source,
403403
// and the second groupby will be applied to the result of the first groupby
404404
// There would be no reason to flatten the properties again if they were already flattened
405-
if (context.FlattenedProperties == null || context.FlattenedProperties.Count == 0)
405+
if (binder is IFlatteningBinder flatteningBinder
406+
&& (context.FlattenedProperties == null || context.FlattenedProperties.Count == 0))
406407
{
407-
AggregationFlatteningResult flatteningResult = binder.FlattenReferencedProperties(
408-
transformationNode,
409-
source,
410-
context);
408+
AggregationFlatteningResult flatteningResult = flatteningBinder.FlattenReferencedProperties(
409+
transformationNode,
410+
source,
411+
context);
411412

412413
if (flatteningResult?.FlattenedExpression != null)
413414
{

src/Microsoft.AspNetCore.OData/Query/Expressions/IAggregationBinder.cs

-42
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@
55
// </copyright>
66
//------------------------------------------------------------------------------
77

8-
using System.Collections.Generic;
9-
using System.Linq;
108
using System.Linq.Expressions;
11-
using Microsoft.OData.UriParser;
129
using Microsoft.OData.UriParser.Aggregation;
1310

1411
namespace Microsoft.AspNetCore.OData.Query.Expressions
@@ -19,45 +16,6 @@ namespace Microsoft.AspNetCore.OData.Query.Expressions
1916
/// </summary>
2017
public interface IAggregationBinder
2118
{
22-
/// <summary>
23-
/// Flattens properties referenced in aggregate clause to avoid generation of nested queries by Entity Framework.
24-
/// </summary>
25-
/// <param name="transformationNode">The OData $apply parse tree represented by <see cref="TransformationNode"/>.</param>
26-
/// <param name="query">The original <see cref="IQueryable"/>.</param>
27-
/// <param name="context">An instance of the <see cref="QueryBinderContext"/> containing the current query context.</param>
28-
/// <returns>
29-
/// An <see cref="AggregationFlatteningResult"/> containing the modified query source and
30-
/// additional metadata resulting from the flattening operation.
31-
/// </returns>
32-
/// <remarks>
33-
/// This method generates a Select expression that flattens the properties referenced in the aggregate clause.
34-
/// Flattening properties helps prevent the generation of nested queries by Entity Framework,
35-
/// resulting in more efficient SQL generation.
36-
/// For query like:
37-
/// <code>
38-
/// groupby((A),aggregate(B/C with max as Alias1,B/D with max as Alias2))
39-
/// </code>
40-
/// generate an expression similar to:
41-
/// <code>
42-
/// $it => new FlatteningWrapper() {
43-
/// Source = $it,
44-
/// Container = new {
45-
/// Value = $it.B.C
46-
/// Next = new {
47-
/// Value = $it.B.D
48-
/// }
49-
/// }
50-
/// }
51-
/// </code>
52-
/// Also populate expressions to access B/C and B/D in aggregate stage to look like:
53-
/// B/C : $it.Container.Value
54-
/// B/D : $it.Container.Next.Value
55-
/// </remarks>
56-
AggregationFlatteningResult FlattenReferencedProperties(
57-
TransformationNode transformationNode,
58-
IQueryable query,
59-
QueryBinderContext context);
60-
6119
/// <summary>
6220
/// Translates an OData $apply parse tree represented by a <see cref="TransformationNode"/> to
6321
/// a LINQ <see cref="Expression"/> that performs a GroupBy operation.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
//-----------------------------------------------------------------------------
2+
// <copyright file="IFlatteningBinder.cs" company=".NET Foundation">
3+
// Copyright (c) .NET Foundation and Contributors. All rights reserved.
4+
// See License.txt in the project root for license information.
5+
// </copyright>
6+
//------------------------------------------------------------------------------
7+
8+
using System.Linq;
9+
using Microsoft.OData.UriParser.Aggregation;
10+
11+
namespace Microsoft.AspNetCore.OData.Query.Expressions
12+
{
13+
/// <summary>
14+
/// Provides an abstraction for flattening property access expressions within an OData $apply clause
15+
/// to support efficient translation of aggregation pipelines in LINQ providers like Entity Framework.
16+
/// </summary>
17+
/// <remarks>
18+
/// Entity Framework versions earlier than EF Core 6.0 may generate nested queries when accessing navigation properties
19+
/// in aggregation clauses. Flattening these properties can help generate flatter, more efficient SQL queries.
20+
/// This interface allows conditional support for flattening based on the capabilities of the underlying LINQ provider.
21+
/// </remarks>
22+
public interface IFlatteningBinder
23+
{
24+
/// <summary>
25+
/// Flattens properties referenced in aggregate clause to avoid generation of nested queries by Entity Framework.
26+
/// </summary>
27+
/// <param name="transformationNode">The OData $apply parse tree represented by <see cref="TransformationNode"/>.</param>
28+
/// <param name="query">The original <see cref="IQueryable"/>.</param>
29+
/// <param name="context">An instance of the <see cref="QueryBinderContext"/> containing the current query context.</param>
30+
/// <returns>
31+
/// An <see cref="AggregationFlatteningResult"/> containing the modified query source and
32+
/// additional metadata resulting from the flattening operation.
33+
/// </returns>
34+
/// <remarks>
35+
/// This method generates a Select expression that flattens the properties referenced in the aggregate clause.
36+
/// Flattening properties helps prevent the generation of nested queries by Entity Framework,
37+
/// resulting in more efficient SQL generation.
38+
/// For query like:
39+
/// <code>
40+
/// groupby((A),aggregate(B/C with max as Alias1,B/D with max as Alias2))
41+
/// </code>
42+
/// generate an expression similar to:
43+
/// <code>
44+
/// $it => new FlatteningWrapper() {
45+
/// Source = $it,
46+
/// Container = new {
47+
/// Value = $it.B.C
48+
/// Next = new {
49+
/// Value = $it.B.D
50+
/// }
51+
/// }
52+
/// }
53+
/// </code>
54+
/// Also populate expressions to access B/C and B/D in aggregate stage to look like:
55+
/// B/C : $it.Container.Value
56+
/// B/D : $it.Container.Next.Value
57+
/// </remarks>
58+
AggregationFlatteningResult FlattenReferencedProperties(
59+
TransformationNode transformationNode,
60+
IQueryable query,
61+
QueryBinderContext context);
62+
}
63+
}

src/Microsoft.AspNetCore.OData/Query/Validator/QueryBinderValidator.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public static void ValidateFlatteningResult(AggregationFlatteningResult flatteni
100100
/// Validates that the provided type implements <see cref="IGroupByWrapper{TContainer, TWrapper}"/> and <see cref="IFlatteningWrapper{T}"/>, and inherits from <see cref="DynamicTypeWrapper"/>.
101101
/// </summary>
102102
/// <param name="flattenedExpressionType">The type representing the flattened expression returned by
103-
/// <see cref="IAggregationBinder.FlattenReferencedProperties(TransformationNode, IQueryable, QueryBinderContext)"/>.</param>
103+
/// <see cref="IFlatteningBinder.FlattenReferencedProperties(TransformationNode, IQueryable, QueryBinderContext)"/>.</param>
104104
/// <exception cref="InvalidOperationException">Thrown if <paramref name="flattenedExpressionType"/>
105105
/// does not implement the required interfaces or inherit from the required base class.</exception>
106106
public static void ValidateFlattenedExpressionType(Type flattenedExpressionType)

test/Microsoft.AspNetCore.OData.E2E.Tests/DollarApply/DollarApplyController.cs

+4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarApply
1515
{
1616
[Route("default")]
1717
[Route("custom")]
18+
[Route("nonflattening")]
1819
public class InMemorySalesController : ODataController
1920
{
2021
private readonly DollarApplyDbContext db;
@@ -35,6 +36,7 @@ public ActionResult<IQueryable<Sale>> GetInMemorySales()
3536

3637
[Route("defaultsql")]
3738
[Route("customsql")]
39+
[Route("nonflatteningsql")]
3840
public class SqlSalesController : ODataController
3941
{
4042
private readonly DollarApplySqlDbContext db;
@@ -55,6 +57,7 @@ public ActionResult<IQueryable<Sale>> GetSqlSales()
5557

5658
[Route("default")]
5759
[Route("custom")]
60+
[Route("nonflattening")]
5861
public class InMemoryProductsController : ODataController
5962
{
6063
private readonly DollarApplyDbContext db;
@@ -75,6 +78,7 @@ public ActionResult<IQueryable<Product>> Get()
7578

7679
[Route("defaultsql")]
7780
[Route("customsql")]
81+
[Route("nonflatteningsql")]
7882
public class SqlProductsController : ODataController
7983
{
8084
private readonly DollarApplySqlDbContext db;

0 commit comments

Comments
 (0)