Skip to content

Commit

Permalink
Implement entity equality Contains() support
Browse files Browse the repository at this point in the history
Fixes #15939
  • Loading branch information
roji committed Jun 18, 2019
1 parent 7839c87 commit 03dfa25
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 2 deletions.
8 changes: 8 additions & 0 deletions src/EFCore/Properties/CoreStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/EFCore/Properties/CoreStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1177,4 +1177,7 @@
<data name="SubqueryWithCompositeKeyNotSupported" xml:space="preserve">
<value>This query would cause multiple evaluation of a subquery because entity '{entityType}' has a composite key. Rewrite your query avoiding the subquery.</value>
</data>
<data name="EntityEqualityContainsWithCompositeKeyNotSupported" xml:space="preserve">
<value>Cannot translate a Contains() operator on entity '{entityType}' because it has a composite key.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Query.Expressions.Internal;
using Microsoft.EntityFrameworkCore.Query.Internal;
using Microsoft.EntityFrameworkCore.Query.NavigationExpansion;

namespace Microsoft.EntityFrameworkCore.Query.Pipeline
{
Expand Down Expand Up @@ -153,8 +154,11 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
{
switch (methodCallExpression.Method.Name)
{
// These are methods that require special handling
case nameof(Queryable.Contains) when arguments.Count == 2:
return VisitContainsMethodCall(methodCallExpression);

// The following are projecting methods, which flow the entity type from *within* the lambda outside.
// These are handled by dedicated methods
case nameof(Queryable.Select):
case nameof(Queryable.SelectMany):
return VisitSelectMethodCall(methodCallExpression);
Expand Down Expand Up @@ -238,6 +242,53 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
return methodCallExpression.Update(Unwrap(Visit(methodCallExpression.Object)), newArguments);
}

protected virtual Expression VisitContainsMethodCall(MethodCallExpression methodCallExpression)
{
var arguments = methodCallExpression.Arguments;
var newSource = Visit(arguments[0]);
var newItem = Visit(arguments[1]);

var sourceEntityType = (newSource as EntityReferenceExpression)?.EntityType;
var itemEntityType = (newItem as EntityReferenceExpression)?.EntityType;

if (sourceEntityType == null && itemEntityType == null)
{
return methodCallExpression.Update(null, new[] { newSource, newItem });
}

if (sourceEntityType != null && itemEntityType != null
&& sourceEntityType.RootType() != itemEntityType.RootType())
{
return Expression.Constant(false);
}

// One side of the comparison may have an unknown entity type (closure parameter, inline instantiation)
var entityType = sourceEntityType ?? itemEntityType;

var keyProperties = entityType.FindPrimaryKey().Properties;
var keyProperty = keyProperties.Count == 1
? keyProperties.Single()
: throw new NotSupportedException(CoreStrings.EntityEqualityContainsWithCompositeKeyNotSupported(entityType.DisplayName()));

// Wrap the source with a projection to its primary key, and the item with a primary key access expression
var param = Expression.Parameter(entityType.ClrType, "v");
var keySelector = Expression.Lambda(param.CreateEFPropertyExpression(keyProperty, makeNullable: false), param);
var keyProjection = Expression.Call(
LinqMethodHelpers.QueryableSelectMethodInfo.MakeGenericMethod(entityType.ClrType, keyProperty.ClrType),
Unwrap(newSource),
keySelector);

var rewrittenItem = newItem.IsNullConstantExpression()
? Expression.Constant(null)
: Unwrap(newItem).CreateEFPropertyExpression(keyProperty, makeNullable: false);

return Expression.Call(
LinqMethodHelpers.QueryableContainsMethodInfo.MakeGenericMethod(keyProperty.ClrType),
keyProjection,
rewrittenItem
);
}

protected virtual Expression VisitSelectMethodCall(MethodCallExpression methodCallExpression)
{
var arguments = methodCallExpression.Arguments;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1736,7 +1736,7 @@ public virtual Task OrderBy_Skip_Last_gives_correct_result(bool isAsync)
entryCount: 1);
}

[ConditionalFact(Skip = "#15939")]
[ConditionalFact(Skip = "#15855")]
public virtual void Contains_over_entityType_should_rewrite_to_identity_equality()
{
using (var context = CreateContext())
Expand All @@ -1749,6 +1749,19 @@ var query
}
}

[ConditionalFact(Skip = "#15855")]
public virtual void Contains_over_entityType_with_null_should_rewrite_to_identity_equality()
{
using (var context = CreateContext())
{
var query
= context.Orders.Where(o => o.CustomerID == "VINET")
.Contains(null);

Assert.True(query);
}
}

[ConditionalFact(Skip = "Issue #14935. Cannot eval 'Contains(__p_0)'")]
public virtual void Contains_over_entityType_should_materialize_when_composite()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1209,6 +1209,14 @@ THEN CAST(1 AS bit) ELSE CAST(0 AS bit)
END");
}

public override void Contains_over_entityType_with_null_should_rewrite_to_identity_equality()
{
base.Contains_over_entityType_with_null_should_rewrite_to_identity_equality();

AssertSql(
@"TODO");
}

public override void Contains_over_entityType_should_materialize_when_composite()
{
base.Contains_over_entityType_should_materialize_when_composite();
Expand Down

0 comments on commit 03dfa25

Please sign in to comment.