Skip to content

Commit 1d63aba

Browse files
fix duplicate join issues (#993)
* fix duplicate join issues * update based on review comments * remove white space * update changes * update changes * update based on review comments
1 parent e7ae0ce commit 1d63aba

File tree

5 files changed

+115
-2
lines changed

5 files changed

+115
-2
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9624,6 +9624,11 @@
96249624
Basically for $compute in $select and $expand
96259625
</summary>
96269626
</member>
9627+
<member name="P:Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext.QueryProvider">
9628+
<summary>
9629+
Gets or sets the query provider.
9630+
</summary>
9631+
</member>
96279632
<member name="M:Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext.GetParameter(System.String)">
96289633
<summary>
96299634
Gets the parameter using parameter name.

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,11 @@ public static IQueryable ApplyBind(this ISelectExpandBinder binder, IQueryable s
273273
throw Error.ArgumentNull(nameof(context));
274274
}
275275

276+
if (string.IsNullOrEmpty(context.QueryProvider))
277+
{
278+
context.QueryProvider = source.Provider.GetType().Namespace;
279+
}
280+
276281
Type elementType = context.ElementClrType;
277282

278283
LambdaExpression projectionLambda = binder.BindSelectExpand(selectExpandClause, context) as LambdaExpression;

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ public QueryBinderContext(QueryBinderContext context, ODataQuerySettings querySe
100100

101101
ElementType = Model.GetEdmTypeReference(ElementClrType)?.Definition;
102102

103+
QueryProvider = context.QueryProvider;
104+
103105
if (ElementType == null)
104106
{
105107
throw new ODataException(Error.Format(SRResources.ClrTypeNotInModel, ElementClrType.FullName));
@@ -168,6 +170,11 @@ public QueryBinderContext(QueryBinderContext context, ODataQuerySettings querySe
168170
/// </summary>
169171
public Expression Source { get;set; }
170172

173+
/// <summary>
174+
/// Gets or sets the query provider.
175+
/// </summary>
176+
internal string QueryProvider { get; set; }
177+
171178
/// <summary>
172179
/// Gets the parameter using parameter name.
173180
/// </summary>

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

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,9 +368,23 @@ internal Expression ProjectElement(QueryBinderContext context, Expression source
368368
bool isSelectedAll = IsSelectAll(selectExpandClause);
369369
if (isSelectedAll)
370370
{
371+
// If we have an EF query provider, then we remove the non-structural properties. The reason for this is to avoid
372+
// Creating an expression that will generate duplicate LEFT joins when a LEFT join already exists in the IQueryable
373+
// as observed here: https://github.com/OData/AspNetCoreOData/issues/497
374+
375+
Expression updatedExpression = null;
376+
if (IsEfQueryProvider(context))
377+
{
378+
updatedExpression = SelectExpandBinder.RemoveNonStructucalProperties(source, structuredType);
379+
}
380+
else
381+
{
382+
updatedExpression = source;
383+
}
384+
371385
// Initialize property 'Instance' on the wrapper class
372386
wrapperProperty = wrapperType.GetProperty("Instance");
373-
wrapperTypeMemberAssignments.Add(Expression.Bind(wrapperProperty, source));
387+
wrapperTypeMemberAssignments.Add(Expression.Bind(wrapperProperty, updatedExpression));
374388

375389
wrapperProperty = wrapperType.GetProperty("UseInstanceForProperties");
376390
wrapperTypeMemberAssignments.Add(Expression.Bind(wrapperProperty, Expression.Constant(true)));
@@ -427,6 +441,51 @@ internal Expression ProjectElement(QueryBinderContext context, Expression source
427441
return Expression.MemberInit(Expression.New(wrapperType), wrapperTypeMemberAssignments);
428442
}
429443

444+
// Generates the expression
445+
// { Instance = new Customer() {Id = $it.Id, Name= $it.Name}}
446+
private static Expression RemoveNonStructucalProperties(Expression source, IEdmStructuredType structuredType)
447+
{
448+
Expression updatedSource = null;
449+
450+
Type elementType = source.Type;
451+
IEnumerable<IEdmStructuralProperty> structuralProperties = structuredType.StructuralProperties();
452+
453+
PropertyInfo[] props = elementType.GetProperties();
454+
List<MemberBinding> bindings = new List<MemberBinding>();
455+
456+
foreach (PropertyInfo prop in props)
457+
{
458+
foreach (var sp in structuralProperties)
459+
{
460+
if (sp.Name == prop.Name)
461+
{
462+
MemberExpression propertyExpression = Expression.Property(source, prop);
463+
bindings.Add(Expression.Bind(prop, propertyExpression));
464+
break;
465+
}
466+
}
467+
}
468+
469+
NewExpression newExpression = Expression.New(source.Type);
470+
updatedSource = Expression.MemberInit(newExpression, bindings);
471+
472+
return updatedSource;
473+
}
474+
475+
private bool IsEfQueryProvider(QueryBinderContext context)
476+
{
477+
if (context.QueryProvider != null &&
478+
context.QueryProvider == HandleNullPropagationOptionHelper.EntityFrameworkQueryProviderNamespace ||
479+
context.QueryProvider == HandleNullPropagationOptionHelper.ObjectContextQueryProviderNamespaceEF5 ||
480+
context.QueryProvider == HandleNullPropagationOptionHelper.ObjectContextQueryProviderNamespaceEF6 ||
481+
context.QueryProvider == HandleNullPropagationOptionHelper.ObjectContextQueryProviderNamespaceEFCore2)
482+
{
483+
return true;
484+
}
485+
486+
return false;
487+
}
488+
430489
private static bool ParseComputedDynamicProperties(QueryBinderContext context, IList<DynamicPathSegment> dynamicPathSegments, bool isSelectedAll,
431490
out IList<string> computedProperties)
432491
{

test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/SelectExpandBinderTest.cs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
using System.Linq.Expressions;
1414
using System.Reflection;
1515
using System.Runtime.Serialization;
16-
using System.Xml.Linq;
1716
using Microsoft.AspNetCore.OData.Edm;
1817
using Microsoft.AspNetCore.OData.Query;
1918
using Microsoft.AspNetCore.OData.Query.Container;
@@ -209,6 +208,44 @@ public void Bind_GeneratedExpression_ContainsExpandedObject()
209208
Assert.Same(_queryable.First(), innerInnerCustomer.Instance);
210209
}
211210

211+
[Theory]
212+
[InlineData(HandleNullPropagationOptionHelper.EntityFrameworkQueryProviderNamespace)]
213+
[InlineData(HandleNullPropagationOptionHelper.ObjectContextQueryProviderNamespaceEFCore2)]
214+
[InlineData(HandleNullPropagationOptionHelper.ObjectContextQueryProviderNamespaceEF5)]
215+
[InlineData(HandleNullPropagationOptionHelper.ObjectContextQueryProviderNamespaceEF6)]
216+
public void Bind_UsingEFQueryProvider_GeneratedExpression__DoesNot_ContainExpandedObject(string queryProvider)
217+
{
218+
// Arrange
219+
SelectExpandQueryOption selectExpand = new SelectExpandQueryOption("Orders", "Orders,Orders($expand=Customer)", _context);
220+
221+
// Act
222+
SelectExpandBinder binder = new SelectExpandBinder();
223+
_queryBinderContext.QueryProvider = queryProvider;
224+
IQueryable queryable = binder.ApplyBind(_queryable, selectExpand.SelectExpandClause, _queryBinderContext);
225+
226+
// Assert
227+
IEnumerator enumerator = queryable.GetEnumerator();
228+
Assert.True(enumerator.MoveNext());
229+
var partialCustomer = Assert.IsAssignableFrom<SelectExpandWrapper<QueryCustomer>>(enumerator.Current);
230+
Assert.False(enumerator.MoveNext());
231+
Assert.Null(partialCustomer.Instance);
232+
Assert.Equal("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryCustomer", partialCustomer.InstanceType);
233+
IEnumerable<SelectExpandWrapper<QueryOrder>> innerOrders = partialCustomer.Container
234+
.ToDictionary(PropertyMapper)["Orders"] as IEnumerable<SelectExpandWrapper<QueryOrder>>;
235+
Assert.NotNull(innerOrders);
236+
SelectExpandWrapper<QueryOrder> partialOrder = innerOrders.Single();
237+
238+
// We only write structural properties to the instance.
239+
// This means that navigation properties on the instance property will be null
240+
// when using any instance of EF query provider.
241+
Assert.Null(partialOrder.Instance.Customer);
242+
243+
object customer = partialOrder.Container.ToDictionary(PropertyMapper)["Customer"];
244+
SelectExpandWrapper<QueryCustomer> innerInnerCustomer = Assert.IsAssignableFrom<SelectExpandWrapper<QueryCustomer>>(customer);
245+
246+
Assert.Null(innerInnerCustomer.Instance.Orders);
247+
}
248+
212249
[Fact]
213250
public void Bind_GeneratedExpression_CheckNullObjectWithinChainProjectionByKey()
214251
{

0 commit comments

Comments
 (0)