Skip to content

Commit

Permalink
Entity equality support for non-extension Contains
Browse files Browse the repository at this point in the history
EE handled the extension version of {IQueryable,IEnumerable}.Contains,
but not instance methods such as List.Contains.

Fixes #15554
  • Loading branch information
roji committed Aug 4, 2019
1 parent 5909486 commit 364ec45
Show file tree
Hide file tree
Showing 10 changed files with 282 additions and 60 deletions.
2 changes: 0 additions & 2 deletions src/EFCore/Extensions/Internal/ExpressionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@
using System.Linq.Expressions;
using System.Reflection;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Query.Internal;
using Microsoft.EntityFrameworkCore.Utilities;

Expand Down
12 changes: 10 additions & 2 deletions src/EFCore/Properties/CoreStrings.Designer.cs

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

5 changes: 4 additions & 1 deletion src/EFCore/Properties/CoreStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1180,12 +1180,15 @@
<data name="PropertyClashingNonIndexer" xml:space="preserve">
<value>The indexed property '{property}' cannot be added to type '{entityType}' because the CLR class contains a member with the same name.</value>
</data>
<data name="SubqueryWithCompositeKeyNotSupported" xml:space="preserve">
<data name="EntityEqualitySubqueryWithCompositeKeyNotSupported" 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>
<data name="EntityEqualityOnKeylessEntityNotSupported" xml:space="preserve">
<value>Comparison on entity type '{entityType}' is not supported because it is a keyless entity.</value>
</data>
<data name="UnableToDiscriminate" xml:space="preserve">
<value>Unable to materialize entity of type '{entityType}'. No discriminators matched '{discriminator}'.</value>
</data>
Expand Down
193 changes: 145 additions & 48 deletions src/EFCore/Query/Internal/EntityEqualityRewritingExpressionVisitor.cs

Large diffs are not rendered by default.

9 changes: 4 additions & 5 deletions src/EFCore/Query/QueryCompilationContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,11 @@ public virtual Func<QueryContext, TResult> CreateQueryExecutor<TResult>(Expressi
/// A lambda must be provided, which will extract the parameter's value from the QueryContext every time
/// the query is executed.
/// </summary>
public virtual void RegisterRuntimeParameter(string name, LambdaExpression valueExtractor)
public virtual ParameterExpression RegisterRuntimeParameter(string name, LambdaExpression valueExtractor, Type type)
{
if (valueExtractor.Parameters.Count != 1
|| valueExtractor.Parameters[0] != QueryContextParameter
|| valueExtractor.ReturnType != typeof(object))
if (valueExtractor.Parameters.Count != 1 || valueExtractor.Parameters[0] != QueryContextParameter)
{
throw new ArgumentException("Runtime parameter extraction lambda must have one QueryContext parameter and return an object",
throw new ArgumentException("Runtime parameter extraction lambda must have one QueryContext parameter",
nameof(valueExtractor));
}

Expand All @@ -110,6 +108,7 @@ public virtual void RegisterRuntimeParameter(string name, LambdaExpression value
}

_runtimeParameters[name] = valueExtractor;
return Expression.Parameter(type, name);
}

private Expression InsertRuntimeParameters(Expression query)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1207,6 +1207,39 @@ FROM root c
WHERE ((c[""Discriminator""] = ""Order"") AND (c[""OrderID""] = 10248))");
}

[ConditionalTheory(Skip = "Issue#14935 (Contains not implemented)")]
public override async Task List_Contains_over_entityType_should_rewrite_to_identity_equality(bool isAsync)
{
await base.List_Contains_over_entityType_should_rewrite_to_identity_equality(isAsync);

AssertSql(
@"SELECT c
FROM root c
WHERE ((c[""Discriminator""] = ""Order"") AND (c[""OrderID""] = 10248))");
}

[ConditionalTheory(Skip = "Issue#14935 (Contains not implemented)")]
public override async Task List_Contains_with_constant_list(bool isAsync)
{
await base.List_Contains_with_constant_list(isAsync);

AssertSql(
@"SELECT c
FROM root c
WHERE ((c[""Discriminator""] = ""Order"") AND (c[""OrderID""] = 10248))");
}

[ConditionalTheory(Skip = "Issue#14935 (Contains not implemented)")]
public override async Task List_Contains_with_parameter_list(bool isAsync)
{
await base.List_Contains_with_parameter_list(isAsync);

AssertSql(
@"SELECT c
FROM root c
WHERE ((c[""Discriminator""] = ""Order"") AND (c[""OrderID""] = 10248))");
}

[ConditionalTheory(Skip = "Issue#14935 (Contains not implemented)")]
public override void Contains_over_entityType_with_null_should_rewrite_to_identity_equality()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.TestModels.Northwind;
Expand Down Expand Up @@ -1510,6 +1511,53 @@ var query
}
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task List_Contains_over_entityType_should_rewrite_to_identity_equality(bool isAsync)
{
var someOrder = new Order { OrderID = 10248 };

return AssertQuery<Customer>(isAsync, cs =>
cs.Where(c => c.Orders.Contains(someOrder)),
entryCount: 1);
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task List_Contains_with_constant_list(bool isAsync)
{
return AssertQuery<Customer>(isAsync, cs =>
cs.Where(c => new List<Customer>
{
new Customer { CustomerID = "ALFKI" },
new Customer { CustomerID = "ANATR" }
}.Contains(c)),
entryCount: 2);
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task List_Contains_with_parameter_list(bool isAsync)
{
var customers = new List<Customer>
{
new Customer { CustomerID = "ALFKI" },
new Customer { CustomerID = "ANATR" }
};

return AssertQuery<Customer>(isAsync, cs => cs.Where(c => customers.Contains(c)),
entryCount: 2);
}

[ConditionalFact]
public virtual void Contains_over_keyless_entity_throws()
{
using (var context = CreateContext())
{
Assert.Throws<InvalidOperationException>(() => context.CustomerQueries.Contains(new CustomerView()));
}
}

[ConditionalFact]
public virtual void Contains_over_entityType_with_null_should_rewrite_to_identity_equality()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,7 @@ where c.Orders.FirstOrDefault() != null
[ConditionalFact]
public virtual void Entity_equality_through_subquery_composite_key()
{
Assert.Throws<NotSupportedException>(() =>
Assert.Throws<InvalidOperationException>(() =>
CreateContext().Orders
.Where(o => o.OrderDetails.FirstOrDefault() == new OrderDetail
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public override bool Equals(object obj)

public override int GetHashCode()
// ReSharper disable once NonReadonlyMemberInGetHashCode
=> CompanyName.GetHashCode();
=> CompanyName?.GetHashCode() ?? 0;

public override string ToString()
=> "CustomerView " + CompanyName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1189,6 +1189,42 @@ ELSE CAST(0 AS bit)
END");
}

public override async Task List_Contains_over_entityType_should_rewrite_to_identity_equality(bool isAsync)
{
await base.List_Contains_over_entityType_should_rewrite_to_identity_equality(isAsync);

AssertSql(
@"@__entity_equality_someOrder_0_OrderID='10248'
SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region]
FROM [Customers] AS [c]
WHERE @__entity_equality_someOrder_0_OrderID IN (
SELECT [o].[OrderID]
FROM [Orders] AS [o]
WHERE ([c].[CustomerID] = [o].[CustomerID]) AND [o].[CustomerID] IS NOT NULL
)");
}

public override async Task List_Contains_with_constant_list(bool isAsync)
{
await base.List_Contains_with_constant_list(isAsync);

AssertSql(
@"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region]
FROM [Customers] AS [c]
WHERE [c].[CustomerID] IN (N'ALFKI', N'ANATR')");
}

public override async Task List_Contains_with_parameter_list(bool isAsync)
{
await base.List_Contains_with_parameter_list(isAsync);

AssertSql(
@"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region]
FROM [Customers] AS [c]
WHERE [c].[CustomerID] IN (N'ALFKI', N'ANATR')");
}

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

0 comments on commit 364ec45

Please sign in to comment.