diff --git a/src/EFCore/Metadata/Internal/PropertyBase.cs b/src/EFCore/Metadata/Internal/PropertyBase.cs index fcaabd22dbf..f15a680e016 100644 --- a/src/EFCore/Metadata/Internal/PropertyBase.cs +++ b/src/EFCore/Metadata/Internal/PropertyBase.cs @@ -290,7 +290,7 @@ private void UpdateFieldInfoConfigurationSource(ConfigurationSource configuratio /// public virtual IClrPropertyGetter Getter => NonCapturingLazyInitializer.EnsureInitialized( - ref _getter, this,p => new ClrPropertyGetterFactory().Create(p)); + ref _getter, this, p => new ClrPropertyGetterFactory().Create(p)); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -300,7 +300,7 @@ private void UpdateFieldInfoConfigurationSource(ConfigurationSource configuratio /// public virtual IClrPropertySetter Setter => NonCapturingLazyInitializer.EnsureInitialized( - ref _setter, this,p => new ClrPropertySetterFactory().Create(p)); + ref _setter, this, p => new ClrPropertySetterFactory().Create(p)); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore/Query/Pipeline/EntityEqualityRewritingExpressionVisitor.cs b/src/EFCore/Query/Pipeline/EntityEqualityRewritingExpressionVisitor.cs index 5892fcbd30f..670b333d155 100644 --- a/src/EFCore/Query/Pipeline/EntityEqualityRewritingExpressionVisitor.cs +++ b/src/EFCore/Query/Pipeline/EntityEqualityRewritingExpressionVisitor.cs @@ -10,37 +10,47 @@ using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Query.Expressions.Internal; using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.EntityFrameworkCore.Query.NavigationExpansion; +using Microsoft.EntityFrameworkCore.Query.NavigationExpansion.Visitors; namespace Microsoft.EntityFrameworkCore.Query.Pipeline { /// - /// Rewrites comparisons of entities (as opposed to comparisons of their properties) into comparison of their keys. + /// Rewrites comparisons of entities (as opposed to comparisons of their properties) into comparison of their keys. /// /// - /// For example, an expression such as cs.Where(c => c == something) would be rewritten to cs.Where(c => c.Id == something.Id). + /// For example, an expression such as cs.Where(c => c == something) would be rewritten to cs.Where(c => c.Id == something.Id). /// public class EntityEqualityRewritingExpressionVisitor : ExpressionVisitor { - protected IDiagnosticsLogger Logger { get; } - protected IModel Model { get; } + /// + /// If the entity equality visitors introduces new runtime parameters (because it adds key access over existing parameters), + /// those parameters will have this prefix. + /// + private const string RuntimeParameterPrefix = CompiledQueryCache.CompiledQueryParameterPrefix + "entity_equality_"; + + private readonly QueryCompilationContext _queryCompilationContext; + private readonly IDiagnosticsLogger _logger; private static readonly MethodInfo _objectEqualsMethodInfo = typeof(object).GetRuntimeMethod(nameof(object.Equals), new[] { typeof(object), typeof(object) }); public EntityEqualityRewritingExpressionVisitor(QueryCompilationContext queryCompilationContext) { - Model = queryCompilationContext.Model; - Logger = queryCompilationContext.Logger; + _queryCompilationContext = queryCompilationContext; + _logger = queryCompilationContext.Logger; } public Expression Rewrite(Expression expression) => Unwrap(Visit(expression)); protected override Expression VisitConstant(ConstantExpression constantExpression) => constantExpression.IsEntityQueryable() - ? new EntityReferenceExpression(constantExpression, Model.FindEntityType(((IQueryable)constantExpression.Value).ElementType)) + ? new EntityReferenceExpression( + constantExpression, + _queryCompilationContext.Model.FindEntityType(((IQueryable)constantExpression.Value).ElementType)) : (Expression)constantExpression; protected override Expression VisitNew(NewExpression newExpression) @@ -278,7 +288,7 @@ protected virtual Expression VisitContainsMethodCall(MethodCallExpression method // 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 keySelector = Expression.Lambda(CreatePropertyAccessExpression(param, keyProperty), param); var keyProjection = Expression.Call( LinqMethodHelpers.QueryableSelectMethodInfo.MakeGenericMethod(entityType.ClrType, keyProperty.ClrType), Unwrap(newSource), @@ -286,7 +296,7 @@ protected virtual Expression VisitContainsMethodCall(MethodCallExpression method var rewrittenItem = newItem.IsNullConstantExpression() ? Expression.Constant(null) - : Unwrap(newItem).CreateEFPropertyExpression(keyProperty, makeNullable: false); + : CreatePropertyAccessExpression(Unwrap(newItem), keyProperty); return Expression.Call( LinqMethodHelpers.QueryableContainsMethodInfo.MakeGenericMethod(keyProperty.ClrType), @@ -333,7 +343,7 @@ protected virtual Expression VisitOrderingMethodCall(MethodCallExpression method var rewrittenKeySelector = Expression.Lambda( ReplacingExpressionVisitor.Replace( oldParam, param, - body.CreateEFPropertyExpression(keyProperty, makeNullable: false)), + CreatePropertyAccessExpression(body, keyProperty)), param); var orderingMethodInfo = GetOrderingMethodInfo(firstOrdering, isAscending); @@ -499,8 +509,8 @@ protected virtual Expression VisitJoinMethodCall(MethodCallExpression methodCall } /// - /// Replaces the lambda's single parameter with a type wrapper based on the given source, and then visits - /// the lambda's body. + /// Replaces the lambda's single parameter with a type wrapper based on the given source, and then visits + /// the lambda's body. /// protected LambdaExpression RewriteAndVisitLambda(LambdaExpression lambda, EntityReferenceExpression source) => Expression.Lambda( @@ -513,8 +523,8 @@ protected LambdaExpression RewriteAndVisitLambda(LambdaExpression lambda, Entity lambda.Parameters); /// - /// Replaces the lambda's two parameters with type wrappers based on the given sources, and then visits - /// the lambda's body. + /// Replaces the lambda's two parameters with type wrappers based on the given sources, and then visits + /// the lambda's body. /// protected LambdaExpression RewriteAndVisitLambda(LambdaExpression lambda, EntityReferenceExpression source1, @@ -529,10 +539,10 @@ protected LambdaExpression RewriteAndVisitLambda(LambdaExpression lambda, lambda.Parameters); /// - /// Receives already-visited left and right operands of an equality expression and applies entity equality rewriting to them, - /// if possible. + /// Receives already-visited left and right operands of an equality expression and applies entity equality rewriting to them, + /// if possible. /// - /// The rewritten entity equality expression, or null if rewriting could not occur for some reason. + /// The rewritten entity equality expression, or null if rewriting could not occur for some reason. protected virtual Expression RewriteEquality(bool equality, Expression left, Expression right) { // TODO: Consider throwing if a child has no flowed entity type, but has a Type that corresponds to an entity type on the model. @@ -597,7 +607,7 @@ private Expression RewriteNullEquality( // collection navigation is only null if its parent entity is null (null propagation thru navigation) // it is probable that user wanted to see if the collection is (not) empty // log warning suggesting to use Any() instead. - Logger.PossibleUnintendedCollectionNavigationNullComparisonWarning(lastNavigation); + _logger.PossibleUnintendedCollectionNavigationNullComparisonWarning(lastNavigation); return RewriteNullEquality(equality, lastNavigation.DeclaringEntityType, UnwrapLastNavigation(nonNullExpression), null); } @@ -609,7 +619,7 @@ private Expression RewriteNullEquality( // (this is also why we can do it even over a subquery with a composite key) return Expression.MakeBinary( equality ? ExpressionType.Equal : ExpressionType.NotEqual, - nonNullExpression.CreateEFPropertyExpression(keyProperties[0]), + CreatePropertyAccessExpression(nonNullExpression, keyProperties[0], makeNullable: true), Expression.Constant(null)); } @@ -625,7 +635,7 @@ private Expression RewriteEntityEquality( if (leftNavigation?.Equals(rightNavigation) == true) { // Log a warning that comparing 2 collections causes reference comparison - Logger.PossibleUnintendedReferenceComparisonWarning(left, right); + _logger.PossibleUnintendedReferenceComparisonWarning(left, right); return RewriteEntityEquality( equality, leftNavigation.DeclaringEntityType, UnwrapLastNavigation(left), null, @@ -688,11 +698,11 @@ protected virtual Expression VisitNullConditional(NullConditionalExpression expr /// doing so can result in application failures when updating to a new Entity Framework Core release. /// // TODO: DRY with NavigationExpansionHelpers - protected static Expression CreateKeyAccessExpression( + protected Expression CreateKeyAccessExpression( [NotNull] Expression target, [NotNull] IReadOnlyList properties) => properties.Count == 1 - ? target.CreateEFPropertyExpression(properties[0]) + ? CreatePropertyAccessExpression(target, properties[0]) : Expression.New( AnonymousObject.AnonymousObjectCtor, Expression.NewArrayInit( @@ -701,11 +711,52 @@ protected static Expression CreateKeyAccessExpression( .Select( p => Expression.Convert( - target.CreateEFPropertyExpression(p), + CreatePropertyAccessExpression(target, p), typeof(object))) .Cast() .ToArray())); + private Expression CreatePropertyAccessExpression(Expression target, IProperty property, bool makeNullable = false) + { + // The target is a constant - evaluate the property immediately and return the result + if (target is ConstantExpression constantExpression) + { + return Expression.Constant(property.GetGetter().GetClrValue(constantExpression.Value), property.ClrType); + } + + // If the target is a query parameter, we can't simply add a property access over it, but must instead cause a new + // parameter to be added at runtime, with the value of the property on the base parameter. + if (target is ParameterExpression baseParameterExpression + && baseParameterExpression.Name.StartsWith(CompiledQueryCache.CompiledQueryParameterPrefix, StringComparison.Ordinal)) + { + // Generate an expression to get the base parameter from the query context's parameter list, and extract the + // property from that + var lambda = Expression.Lambda( + Expression.Call( + _parameterValueExtractor, + QueryCompilationContext.QueryContextParameter, + Expression.Constant(baseParameterExpression.Name, typeof(string)), + Expression.Constant(property, typeof(IProperty))), + QueryCompilationContext.QueryContextParameter + ); + + var newParameterName = $"{RuntimeParameterPrefix}{baseParameterExpression.Name.Substring(CompiledQueryCache.CompiledQueryParameterPrefix.Length)}_{property.Name}"; + _queryCompilationContext.RegisterRuntimeParameter(newParameterName, lambda); + return Expression.Parameter(property.ClrType, newParameterName); + } + + return target.CreateEFPropertyExpression(property, makeNullable); + + } + + static object ParameterValueExtractor(QueryContext context, string baseParameterName, IProperty property) + { + var baseParameter = context.ParameterValues[baseParameterName]; + return baseParameter == null ? null : property.GetGetter().GetClrValue(baseParameter); + } + + private static readonly MethodInfo _parameterValueExtractor + = typeof(EntityEqualityRewritingExpressionVisitor).GetMethod(nameof(ParameterValueExtractor), BindingFlags.NonPublic | BindingFlags.Static); protected static Expression UnwrapLastNavigation(Expression expression) => (expression as MemberExpression)?.Expression @@ -731,7 +782,7 @@ public class EntityReferenceExpression : Expression public override ExpressionType NodeType => ExpressionType.Extension; /// - /// The underlying expression being wrapped. + /// The underlying expression being wrapped. /// [NotNull] public Expression Underlying { get; } @@ -789,9 +840,9 @@ public EntityReferenceExpression( } /// - /// Attempts to find as a navigation from the current node, - /// and if successful, returns a new wrapping the - /// given expression. Otherwise returns the given expression without wrapping it. + /// Attempts to find as a navigation from the current node, + /// and if successful, returns a new wrapping the + /// given expression. Otherwise returns the given expression without wrapping it. /// public virtual Expression TraverseProperty(string propertyName, Expression destinationExpression) { diff --git a/src/EFCore/Query/Pipeline/ParameterExtractingExpressionVisitor.cs b/src/EFCore/Query/Pipeline/ParameterExtractingExpressionVisitor.cs index 4c9c6a6d3f8..2acb85eb3c5 100644 --- a/src/EFCore/Query/Pipeline/ParameterExtractingExpressionVisitor.cs +++ b/src/EFCore/Query/Pipeline/ParameterExtractingExpressionVisitor.cs @@ -175,7 +175,7 @@ private Expression TryGetConstantValue(Expression expression) { if (_evaluatableExpressions.ContainsKey(expression)) { - var value = GetValue(expression, out var _); + var value = GetValue(expression, out _); if (value is bool) { diff --git a/src/EFCore/Query/Pipeline/QueryCompilationContext.cs b/src/EFCore/Query/Pipeline/QueryCompilationContext.cs index 0d2f34eb960..7ad70bafbdf 100644 --- a/src/EFCore/Query/Pipeline/QueryCompilationContext.cs +++ b/src/EFCore/Query/Pipeline/QueryCompilationContext.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; +using System.Reflection; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; @@ -20,6 +22,13 @@ public class QueryCompilationContext private readonly IShapedQueryOptimizerFactory _shapedQueryOptimizerFactory; private readonly IShapedQueryCompilingExpressionVisitorFactory _shapedQueryCompilingExpressionVisitorFactory; + /// + /// A dictionary mapping parameter names to lambdas that, given a QueryContext, can extract that parameter's value. + /// This is needed for cases where we need to introduce a parameter during the compilation phase (e.g. entity equality rewrites + /// a parameter to an ID property on that parameter). + /// + private Dictionary _runtimeParameters; + public QueryCompilationContext( IModel model, IQueryOptimizerFactory queryOptimizerFactory, @@ -42,7 +51,6 @@ public QueryCompilationContext( _queryableMethodTranslatingExpressionVisitorFactory = queryableMethodTranslatingExpressionVisitorFactory; _shapedQueryOptimizerFactory = shapedQueryOptimizerFactory; _shapedQueryCompilingExpressionVisitorFactory = shapedQueryCompilingExpressionVisitorFactory; - } public bool Async { get; } @@ -69,6 +77,10 @@ public virtual Func CreateQueryExecutor(Expressi // Inject tracking query = _shapedQueryCompilingExpressionVisitorFactory.Create(this).Visit(query); + // If any additional parameters were added during the compilation phase (e.g. entity equality ID expression), + // wrap the query with code adding those parameters to the query context + query = InsertRuntimeParameters(query); + var queryExecutorExpression = Expression.Lambda>( query, QueryContextParameter); @@ -82,5 +94,45 @@ public virtual Func CreateQueryExecutor(Expressi Logger.QueryExecutionPlanned(new ExpressionPrinter(), queryExecutorExpression); } } + + /// + /// Registers a runtime parameter that is being added at some point during the compilation phase. + /// A lambda must be provided, which will extract the parameter's value from the QueryContext every time + /// the query is executed. + /// + public void RegisterRuntimeParameter(string name, LambdaExpression valueExtractor) + { + if (valueExtractor.Parameters.Count != 1 + || valueExtractor.Parameters[0] != QueryContextParameter + || valueExtractor.ReturnType != typeof(object)) + { + throw new ArgumentException("Runtime parameter extraction lambda must have one QueryContext parameter and return an object", + nameof(valueExtractor)); + } + + if (_runtimeParameters == null) + { + _runtimeParameters = new Dictionary(); + } + + _runtimeParameters[name] = valueExtractor; + } + + private Expression InsertRuntimeParameters(Expression query) + => _runtimeParameters == null + ? query + : Expression.Block(_runtimeParameters + .Select(kv => + Expression.Call( + QueryContextParameter, + _queryContextAddParameterMethodInfo, + Expression.Constant(kv.Key), + Expression.Invoke(kv.Value, QueryContextParameter))) + .Append(query)); + + private static readonly MethodInfo _queryContextAddParameterMethodInfo + = typeof(QueryContext) + .GetTypeInfo() + .GetDeclaredMethod(nameof(QueryContext.AddParameter)); } } diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/SimpleQueryCosmosTest.ResultOperators.cs b/test/EFCore.Cosmos.FunctionalTests/Query/SimpleQueryCosmosTest.ResultOperators.cs index 6b18edd80a9..e1c88e99cdc 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/SimpleQueryCosmosTest.ResultOperators.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/SimpleQueryCosmosTest.ResultOperators.cs @@ -1196,6 +1196,7 @@ FROM root c WHERE (c[""Discriminator""] = ""Customer"")"); } + [ConditionalTheory(Skip = "Issue#14935 (Contains not implemented)")] public override void Contains_over_entityType_should_rewrite_to_identity_equality() { base.Contains_over_entityType_should_rewrite_to_identity_equality(); @@ -1206,6 +1207,17 @@ 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() + { + base.Contains_over_entityType_with_null_should_rewrite_to_identity_equality(); + + AssertSql( + @"SELECT c +FROM root c +WHERE ((c[""Discriminator""] = ""Order"") AND (c[""OrderID""] = 10248))"); + } + public override void Contains_over_entityType_should_materialize_when_composite() { base.Contains_over_entityType_should_materialize_when_composite(); diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/SimpleQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/SimpleQueryCosmosTest.cs index ab77aac8522..1391bf9b408 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/SimpleQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/SimpleQueryCosmosTest.cs @@ -116,12 +116,26 @@ public override async Task Entity_equality_local(bool isAsync) { await base.Entity_equality_local(isAsync); + AssertSql( + @"@__entity_equality_local_0_CustomerID='ANATR' + +SELECT c[""CustomerID""] +FROM root c +WHERE ((c[""Discriminator""] = ""Customer"") AND (c[""CustomerID""] = @__entity_equality_local_0_CustomerID))"); + } + + [ConditionalTheory(Skip = "Issue#14935")] + public override async Task Entity_equality_local_composite_key(bool isAsync) + { + await base.Entity_equality_local_composite_key(isAsync); + AssertSql( @"SELECT c FROM root c WHERE (c[""Discriminator""] = ""Customer"")"); } + [ConditionalTheory(Skip = "Issue#14935")] public override async Task Join_with_entity_equality_local_on_both_sources(bool isAsync) { await base.Join_with_entity_equality_local_on_both_sources(isAsync); @@ -136,6 +150,17 @@ public override async Task Entity_equality_local_inline(bool isAsync) { await base.Entity_equality_local_inline(isAsync); + AssertSql( + @"SELECT c[""CustomerID""] +FROM root c +WHERE ((c[""Discriminator""] = ""Customer"") AND (c[""CustomerID""] = ""ANATR""))"); + } + + [ConditionalTheory(Skip = "Issue#14935")] + public override async Task Entity_equality_local_inline_composite_key(bool isAsync) + { + await base.Entity_equality_local_inline_composite_key(isAsync); + AssertSql( @"SELECT c FROM root c @@ -321,6 +346,7 @@ FROM root c WHERE (c[""Discriminator""] = ""Employee"")"); } + [ConditionalTheory(Skip = "Issue#14935")] public override async Task Where_query_composition_entity_equality_one_element_FirstOrDefault(bool isAsync) { await base.Where_query_composition_entity_equality_one_element_FirstOrDefault(isAsync); @@ -341,6 +367,7 @@ FROM root c WHERE (c[""Discriminator""] = ""Employee"")"); } + [ConditionalTheory(Skip = "Issue#14935")] public override async Task Where_query_composition_entity_equality_no_elements_FirstOrDefault(bool isAsync) { await base.Where_query_composition_entity_equality_no_elements_FirstOrDefault(isAsync); @@ -351,6 +378,7 @@ FROM root c WHERE (c[""Discriminator""] = ""Employee"")"); } + [ConditionalTheory(Skip = "Issue#14935")] public override async Task Where_query_composition_entity_equality_multiple_elements_FirstOrDefault(bool isAsync) { await base.Where_query_composition_entity_equality_multiple_elements_FirstOrDefault(isAsync); @@ -3721,6 +3749,7 @@ FROM root c WHERE (c[""Discriminator""] = ""Customer"")"); } + [ConditionalTheory(Skip = "Issue#14935")] public override async Task Let_entity_equality_to_other_entity(bool isAsync) { await base.Let_entity_equality_to_other_entity(isAsync); diff --git a/test/EFCore.Relational.Specification.Tests/Query/FromSqlQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/FromSqlQueryTestBase.cs index 7a51000b1d3..7c848694364 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/FromSqlQueryTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/FromSqlQueryTestBase.cs @@ -989,7 +989,7 @@ public virtual void FromSqlRaw_does_not_parameterize_interpolated_string() } } - [ConditionalFact(Skip = "#15855")] + [ConditionalFact] public virtual void Entity_equality_through_fromsql() { using (var context = CreateContext()) @@ -1002,7 +1002,7 @@ public virtual void Entity_equality_through_fromsql() }) .ToArray(); - Assert.Equal(1, actual.Length); + Assert.Equal(5, actual.Length); } } diff --git a/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs index 663da301d0e..d0141b7ab81 100644 --- a/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs @@ -39,7 +39,7 @@ protected ComplexNavigationsQueryTestBase(TFixture fixture) { } - [ConditionalTheory(Skip = "Issue#15855")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Entity_equality_empty(bool isAsync) { @@ -146,7 +146,7 @@ public virtual Task Key_equality_using_property_method_and_member_expression3(bo (e, a) => Assert.Equal(e.Id, a.Id)); } - [ConditionalTheory(Skip = "Issue#15855")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Key_equality_navigation_converted_to_FK(bool isAsync) { @@ -163,7 +163,7 @@ public virtual Task Key_equality_navigation_converted_to_FK(bool isAsync) (e, a) => Assert.Equal(e.Id, a.Id)); } - [ConditionalTheory(Skip = "Issue#15855")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Key_equality_two_conditions_on_same_navigation(bool isAsync) { @@ -185,7 +185,7 @@ public virtual Task Key_equality_two_conditions_on_same_navigation(bool isAsync) (e, a) => Assert.Equal(e.Id, a.Id)); } - [ConditionalTheory(Skip = "Issue#15855")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Key_equality_two_conditions_on_same_navigation2(bool isAsync) { diff --git a/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.ResultOperators.cs b/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.ResultOperators.cs index 8638482a53a..b567a4d5ea6 100644 --- a/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.ResultOperators.cs +++ b/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.ResultOperators.cs @@ -1497,7 +1497,7 @@ public virtual Task OrderBy_Skip_Last_gives_correct_result(bool isAsync) entryCount: 1); } - [ConditionalFact(Skip = "#15855")] + [ConditionalFact] public virtual void Contains_over_entityType_should_rewrite_to_identity_equality() { using (var context = CreateContext()) @@ -1510,7 +1510,7 @@ var query } } - [ConditionalFact(Skip = "#15855")] + [ConditionalFact] public virtual void Contains_over_entityType_with_null_should_rewrite_to_identity_equality() { using (var context = CreateContext()) @@ -1519,7 +1519,7 @@ var query = context.Orders.Where(o => o.CustomerID == "VINET") .Contains(null); - Assert.True(query); + Assert.False(query); } } diff --git a/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.Where.cs b/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.Where.cs index d66f0ff3115..92727b0148c 100644 --- a/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.Where.cs +++ b/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.Where.cs @@ -1517,7 +1517,7 @@ await AssertQuery( entryCount: 1); } - [ConditionalTheory(Skip = "#15855")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual async Task Where_poco_closure(bool isAsync) { @@ -1970,7 +1970,7 @@ public virtual Task Where_subquery_FirstOrDefault_is_null(bool isAsync) entryCount: 2); } - [ConditionalTheory(Skip = "Issue#15855")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Where_subquery_FirstOrDefault_compared_to_entity(bool isAsync) { diff --git a/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.cs index f5c5bdd0d1e..e008446554b 100644 --- a/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.cs @@ -364,7 +364,7 @@ from c in cs select c.CustomerID); } - [ConditionalTheory(Skip = "#15855")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Entity_equality_local(bool isAsync) { @@ -381,9 +381,44 @@ from c in cs select c.CustomerID); } - // issue #12871 - //[ConditionalTheory] - //[MemberData(nameof(IsAsyncData))] + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Entity_equality_local_composite_key(bool isAsync) + { + var local = new OrderDetail + { + OrderID = 10248, + ProductID = 11 + }; + + return AssertQuery( + isAsync, + odt => + from od in odt + where od.Equals(local) + select od, + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Entity_equality_local_double_check(bool isAsync) + { + var local = new Customer + { + CustomerID = "ANATR" + }; + + return AssertQuery( + isAsync, + cs => + from c in cs + where c == local && local == c + select c.CustomerID); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] public virtual Task Join_with_entity_equality_local_on_both_sources(bool isAsync) { var local = new Customer @@ -402,7 +437,7 @@ from c2 in cs select c2, o => o, i => i, (o, i) => o).Select(e => e.CustomerID)); } - [ConditionalTheory(Skip = "#15855")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Entity_equality_local_inline(bool isAsync) { @@ -417,18 +452,18 @@ from c in cs select c.CustomerID); } - [ConditionalTheory(Skip = "#15855")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Entity_equality_local_inline_composite_key(bool isAsync) => AssertQuery( isAsync, odt => from od in odt - where od == new OrderDetail + where od.Equals(new OrderDetail { OrderID = 10248, ProductID = 11 - } + }) select od, entryCount: 1); @@ -1970,7 +2005,7 @@ from e1 in es.Take(3) select e1); } - [ConditionalTheory(Skip = "#15855")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Where_query_composition_entity_equality_one_element_FirstOrDefault(bool isAsync) { @@ -1994,7 +2029,7 @@ from e1 in es.Take(3) select e1); } - [ConditionalFact] + [ConditionalFact(Skip = "#15559")] public virtual void Where_query_composition_entity_equality_no_elements_Single() { using (var ctx = CreateContext()) @@ -2007,7 +2042,7 @@ public virtual void Where_query_composition_entity_equality_no_elements_Single() } } - [ConditionalTheory(Skip = "#15855")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Where_query_composition_entity_equality_no_elements_FirstOrDefault(bool isAsync) { @@ -2019,7 +2054,7 @@ from e1 in es select e1); } - [ConditionalFact] + [ConditionalFact(Skip = "#15559")] public virtual void Where_query_composition_entity_equality_multiple_elements_SingleOrDefault() { using (var ctx = CreateContext()) @@ -2032,7 +2067,7 @@ public virtual void Where_query_composition_entity_equality_multiple_elements_Si } } - [ConditionalTheory(Skip = "#15855")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Where_query_composition_entity_equality_multiple_elements_FirstOrDefault(bool isAsync) { @@ -5821,7 +5856,7 @@ public virtual Task Let_entity_equality_to_null(bool isAsync) }); } - [ConditionalTheory(Skip = "#15855")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Let_entity_equality_to_other_entity(bool isAsync) { diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.ResultOperators.cs b/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.ResultOperators.cs index 8c5e0885f24..54e64a2c4e3 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.ResultOperators.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.ResultOperators.cs @@ -1183,15 +1183,16 @@ public override void Contains_over_entityType_should_rewrite_to_identity_equalit FROM [Orders] AS [o] WHERE [o].[OrderID] = 10248", // - @"@__p_0_OrderID='10248' + @"@__entity_equality_p_0_OrderID='10248' SELECT CASE - WHEN @__p_0_OrderID IN ( + WHEN @__entity_equality_p_0_OrderID IN ( SELECT [o].[OrderID] FROM [Orders] AS [o] - WHERE [o].[CustomerID] = N'VINET' + WHERE ([o].[CustomerID] = N'VINET') AND [o].[CustomerID] IS NOT NULL ) - THEN CAST(1 AS bit) ELSE CAST(0 AS bit) + THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) END"); } @@ -1200,7 +1201,17 @@ public override void Contains_over_entityType_with_null_should_rewrite_to_identi base.Contains_over_entityType_with_null_should_rewrite_to_identity_equality(); AssertSql( - @"TODO"); + @"@__entity_equality_p_0_OrderID='' (Nullable = false) (DbType = Int32) + +SELECT CASE + WHEN @__entity_equality_p_0_OrderID IN ( + SELECT [o].[OrderID] + FROM [Orders] AS [o] + WHERE ([o].[CustomerID] = N'VINET') AND [o].[CustomerID] IS NOT NULL + ) + THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END"); } public override void Contains_over_entityType_should_materialize_when_composite() diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.Where.cs b/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.Where.cs index 5d70cd50ec7..812bdaee31e 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.Where.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.Where.cs @@ -1744,12 +1744,15 @@ public override async Task Where_subquery_FirstOrDefault_compared_to_entity(bool 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 ( +WHERE (( SELECT TOP(1) [o].[OrderID] FROM [Orders] AS [o] - WHERE [c].[CustomerID] = [o].[CustomerID] - ORDER BY [o].[OrderID] -) = 10243"); + WHERE ([c].[CustomerID] = [o].[CustomerID]) AND [o].[CustomerID] IS NOT NULL + ORDER BY [o].[OrderID]) = 10243) AND ( + SELECT TOP(1) [o].[OrderID] + FROM [Orders] AS [o] + WHERE ([c].[CustomerID] = [o].[CustomerID]) AND [o].[CustomerID] IS NOT NULL + ORDER BY [o].[OrderID]) IS NOT NULL"); } public override async Task Time_of_day_datetime(bool isAsync) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.cs index 25884ae75ed..ab11ec00507 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.cs @@ -175,11 +175,36 @@ public override async Task Entity_equality_local(bool isAsync) await base.Entity_equality_local(isAsync); AssertSql( - @"@__local_0_CustomerID='ANATR' (Nullable = false) (Size = 5) + @"@__entity_equality_local_0_CustomerID='ANATR' (Size = 5) SELECT [c].[CustomerID] FROM [Customers] AS [c] -WHERE [c].[CustomerID] = @__local_0_CustomerID"); +WHERE ([c].[CustomerID] = @__entity_equality_local_0_CustomerID) AND @__entity_equality_local_0_CustomerID IS NOT NULL"); + } + + public override async Task Entity_equality_local_composite_key(bool isAsync) + { + await base.Entity_equality_local_composite_key(isAsync); + + AssertSql( + @"@__entity_equality_local_0_OrderID='10248' +@__entity_equality_local_0_ProductID='11' + +SELECT [o].[OrderID], [o].[ProductID], [o].[Discount], [o].[Quantity], [o].[UnitPrice] +FROM [Order Details] AS [o] +WHERE (([o].[OrderID] = @__entity_equality_local_0_OrderID) AND @__entity_equality_local_0_OrderID IS NOT NULL) AND (([o].[ProductID] = @__entity_equality_local_0_ProductID) AND @__entity_equality_local_0_ProductID IS NOT NULL)"); + } + + public override async Task Entity_equality_local_double_check(bool isAsync) + { + await base.Entity_equality_local_double_check(isAsync); + + AssertSql( + @"@__entity_equality_local_0_CustomerID='ANATR' (Size = 5) + +SELECT [c].[CustomerID] +FROM [Customers] AS [c] +WHERE (([c].[CustomerID] = @__entity_equality_local_0_CustomerID) AND @__entity_equality_local_0_CustomerID IS NOT NULL) AND ((@__entity_equality_local_0_CustomerID = [c].[CustomerID]) AND @__entity_equality_local_0_CustomerID IS NOT NULL)"); } public override async Task Join_with_entity_equality_local_on_both_sources(bool isAsync) @@ -187,7 +212,16 @@ public override async Task Join_with_entity_equality_local_on_both_sources(bool await base.Join_with_entity_equality_local_on_both_sources(isAsync); AssertSql( - ""); + @"@__entity_equality_local_0_CustomerID='ANATR' (Size = 5) + +SELECT [c].[CustomerID] +FROM [Customers] AS [c] +INNER JOIN ( + SELECT [c0].[CustomerID], [c0].[Address], [c0].[City], [c0].[CompanyName], [c0].[ContactName], [c0].[ContactTitle], [c0].[Country], [c0].[Fax], [c0].[Phone], [c0].[PostalCode], [c0].[Region] + FROM [Customers] AS [c0] + WHERE ([c0].[CustomerID] = @__entity_equality_local_0_CustomerID) AND @__entity_equality_local_0_CustomerID IS NOT NULL +) AS [t] ON [c].[CustomerID] = [t].[CustomerID] +WHERE ([c].[CustomerID] = @__entity_equality_local_0_CustomerID) AND @__entity_equality_local_0_CustomerID IS NOT NULL"); } public override async Task Entity_equality_local_inline(bool isAsync) @@ -204,7 +238,10 @@ public override async Task Entity_equality_local_inline_composite_key(bool isAsy { await base.Entity_equality_local_inline_composite_key(isAsync); - // TODO: AssertSql + AssertSql( + @"SELECT [o].[OrderID], [o].[ProductID], [o].[Discount], [o].[Quantity], [o].[UnitPrice] +FROM [Order Details] AS [o] +WHERE ([o].[OrderID] = 10248) AND ([o].[ProductID] = 11)"); } public override async Task Entity_equality_null(bool isAsync) @@ -506,13 +543,15 @@ public override async Task Where_query_composition_entity_equality_one_element_F await base.Where_query_composition_entity_equality_one_element_FirstOrDefault(isAsync); AssertSql( - @"SELECT [e1].[EmployeeID], [e1].[City], [e1].[Country], [e1].[FirstName], [e1].[ReportsTo], [e1].[Title] -FROM [Employees] AS [e1] -WHERE ( - SELECT TOP(1) [e2].[EmployeeID] - FROM [Employees] AS [e2] - WHERE [e2].[EmployeeID] = [e1].[ReportsTo] -) = CAST(0 AS bigint)"); + @"SELECT [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title] +FROM [Employees] AS [e] +WHERE (( + SELECT TOP(1) [e0].[EmployeeID] + FROM [Employees] AS [e0] + WHERE ([e0].[EmployeeID] = [e].[ReportsTo]) AND [e].[ReportsTo] IS NOT NULL) = 0) AND ( + SELECT TOP(1) [e0].[EmployeeID] + FROM [Employees] AS [e0] + WHERE ([e0].[EmployeeID] = [e].[ReportsTo]) AND [e].[ReportsTo] IS NOT NULL) IS NOT NULL"); } public override async Task Where_query_composition_entity_equality_no_elements_SingleOrDefault(bool isAsync) @@ -546,13 +585,15 @@ public override async Task Where_query_composition_entity_equality_no_elements_F await base.Where_query_composition_entity_equality_no_elements_FirstOrDefault(isAsync); AssertSql( - @"SELECT [e1].[EmployeeID], [e1].[City], [e1].[Country], [e1].[FirstName], [e1].[ReportsTo], [e1].[Title] -FROM [Employees] AS [e1] -WHERE ( - SELECT TOP(1) [e2].[EmployeeID] - FROM [Employees] AS [e2] - WHERE [e2].[EmployeeID] = 42 -) = CAST(0 AS bigint)"); + @"SELECT [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title] +FROM [Employees] AS [e] +WHERE (( + SELECT TOP(1) [e0].[EmployeeID] + FROM [Employees] AS [e0] + WHERE [e0].[EmployeeID] = 42) = 0) AND ( + SELECT TOP(1) [e0].[EmployeeID] + FROM [Employees] AS [e0] + WHERE [e0].[EmployeeID] = 42) IS NOT NULL"); } public override async Task Where_query_composition_entity_equality_multiple_elements_FirstOrDefault(bool isAsync) @@ -560,13 +601,15 @@ public override async Task Where_query_composition_entity_equality_multiple_elem await base.Where_query_composition_entity_equality_multiple_elements_FirstOrDefault(isAsync); AssertSql( - @"SELECT [e1].[EmployeeID], [e1].[City], [e1].[Country], [e1].[FirstName], [e1].[ReportsTo], [e1].[Title] -FROM [Employees] AS [e1] -WHERE ( - SELECT TOP(1) [e2].[EmployeeID] - FROM [Employees] AS [e2] - WHERE ([e2].[EmployeeID] <> [e1].[ReportsTo]) OR [e1].[ReportsTo] IS NULL -) = CAST(0 AS bigint)"); + @"SELECT [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title] +FROM [Employees] AS [e] +WHERE (( + SELECT TOP(1) [e0].[EmployeeID] + FROM [Employees] AS [e0] + WHERE ([e0].[EmployeeID] <> [e].[ReportsTo]) OR [e].[ReportsTo] IS NULL) = 0) AND ( + SELECT TOP(1) [e0].[EmployeeID] + FROM [Employees] AS [e0] + WHERE ([e0].[EmployeeID] <> [e].[ReportsTo]) OR [e].[ReportsTo] IS NULL) IS NOT NULL"); } public override async Task Where_query_composition2(bool isAsync) @@ -4740,78 +4783,21 @@ public override async Task Let_entity_equality_to_other_entity(bool isAsync) await base.Let_entity_equality_to_other_entity(isAsync); AssertSql( - @"SELECT [c].[CustomerID], CASE - WHEN ( - SELECT TOP(1) [o0].[OrderID] - FROM [Orders] AS [o0] - WHERE [c].[CustomerID] = [o0].[CustomerID] - ORDER BY [o0].[OrderDate] - ) IS NOT NULL - THEN ( - SELECT TOP(1) [o1].[OrderDate] - FROM [Orders] AS [o1] - WHERE [c].[CustomerID] = [o1].[CustomerID] - ORDER BY [o1].[OrderDate] - ) ELSE NULL -END AS [A] + @"SELECT [c].[CustomerID], ( + SELECT TOP(1) [o].[OrderDate] + FROM [Orders] AS [o] + WHERE ([c].[CustomerID] = [o].[CustomerID]) AND [o].[CustomerID] IS NOT NULL + ORDER BY [o].[OrderDate]) AS [A] FROM [Customers] AS [c] -WHERE [c].[CustomerID] LIKE N'A%'", - // - @"@_outer_CustomerID='ALFKI' (Size = 5) - -SELECT TOP(1) [e].[OrderID], [e].[CustomerID], [e].[EmployeeID], [e].[OrderDate] -FROM [Orders] AS [e] -WHERE @_outer_CustomerID = [e].[CustomerID] -ORDER BY [e].[OrderDate]", - // - @"@_outer_CustomerID1='ALFKI' (Size = 5) - -SELECT TOP(1) [e1].[OrderID], [e1].[CustomerID], [e1].[EmployeeID], [e1].[OrderDate] -FROM [Orders] AS [e1] -WHERE @_outer_CustomerID1 = [e1].[CustomerID] -ORDER BY [e1].[OrderDate]", - // - @"@_outer_CustomerID='ANATR' (Size = 5) - -SELECT TOP(1) [e].[OrderID], [e].[CustomerID], [e].[EmployeeID], [e].[OrderDate] -FROM [Orders] AS [e] -WHERE @_outer_CustomerID = [e].[CustomerID] -ORDER BY [e].[OrderDate]", - // - @"@_outer_CustomerID1='ANATR' (Size = 5) - -SELECT TOP(1) [e1].[OrderID], [e1].[CustomerID], [e1].[EmployeeID], [e1].[OrderDate] -FROM [Orders] AS [e1] -WHERE @_outer_CustomerID1 = [e1].[CustomerID] -ORDER BY [e1].[OrderDate]", - // - @"@_outer_CustomerID='ANTON' (Size = 5) - -SELECT TOP(1) [e].[OrderID], [e].[CustomerID], [e].[EmployeeID], [e].[OrderDate] -FROM [Orders] AS [e] -WHERE @_outer_CustomerID = [e].[CustomerID] -ORDER BY [e].[OrderDate]", - // - @"@_outer_CustomerID1='ANTON' (Size = 5) - -SELECT TOP(1) [e1].[OrderID], [e1].[CustomerID], [e1].[EmployeeID], [e1].[OrderDate] -FROM [Orders] AS [e1] -WHERE @_outer_CustomerID1 = [e1].[CustomerID] -ORDER BY [e1].[OrderDate]", - // - @"@_outer_CustomerID='AROUT' (Size = 5) - -SELECT TOP(1) [e].[OrderID], [e].[CustomerID], [e].[EmployeeID], [e].[OrderDate] -FROM [Orders] AS [e] -WHERE @_outer_CustomerID = [e].[CustomerID] -ORDER BY [e].[OrderDate]", - // - @"@_outer_CustomerID1='AROUT' (Size = 5) - -SELECT TOP(1) [e1].[OrderID], [e1].[CustomerID], [e1].[EmployeeID], [e1].[OrderDate] -FROM [Orders] AS [e1] -WHERE @_outer_CustomerID1 = [e1].[CustomerID] -ORDER BY [e1].[OrderDate]"); +WHERE ([c].[CustomerID] LIKE N'A%') AND ((( + SELECT TOP(1) [o0].[OrderID] + FROM [Orders] AS [o0] + WHERE ([c].[CustomerID] = [o0].[CustomerID]) AND [o0].[CustomerID] IS NOT NULL + ORDER BY [o0].[OrderDate]) <> 0) OR ( + SELECT TOP(1) [o0].[OrderID] + FROM [Orders] AS [o0] + WHERE ([c].[CustomerID] = [o0].[CustomerID]) AND [o0].[CustomerID] IS NOT NULL + ORDER BY [o0].[OrderDate]) IS NULL)"); } // public override async Task SelectMany_after_client_method(bool isAsync)