diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryLinqOperatorProvider.cs b/src/EFCore.InMemory/Query/Internal/InMemoryLinqOperatorProvider.cs index 1d7b234eb7b..0d588543b4a 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryLinqOperatorProvider.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryLinqOperatorProvider.cs @@ -59,6 +59,11 @@ private static IEnumerable GetMethods(string name, int parameterCoun public static MethodInfo Single = GetMethod(nameof(Enumerable.Single), 0); public static MethodInfo SingleOrDefault = GetMethod(nameof(Enumerable.SingleOrDefault), 0); + public static MethodInfo Concat = GetMethod(nameof(Enumerable.Concat), 1); + public static MethodInfo Except = GetMethod(nameof(Enumerable.Except), 1); + public static MethodInfo Intersect = GetMethod(nameof(Enumerable.Intersect), 1); + public static MethodInfo Union = GetMethod(nameof(Enumerable.Union), 1); + public static MethodInfo GetAggregateMethod(string methodName, Type elementType, int parameterCount = 0) { Check.NotEmpty(methodName, nameof(methodName)); diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs index be86e1e5367..66543828fb1 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs @@ -126,7 +126,7 @@ protected override ShapedQueryExpression TranslateCast(ShapedQueryExpression sou } protected override ShapedQueryExpression TranslateConcat(ShapedQueryExpression source1, ShapedQueryExpression source2) - => null; + => TranslateSetOperation(InMemoryLinqOperatorProvider.Concat, source1, source2); protected override ShapedQueryExpression TranslateContains(ShapedQueryExpression source, Expression item) { @@ -203,7 +203,7 @@ protected override ShapedQueryExpression TranslateElementAtOrDefault(ShapedQuery => null; protected override ShapedQueryExpression TranslateExcept(ShapedQueryExpression source1, ShapedQueryExpression source2) - => null; + => TranslateSetOperation(InMemoryLinqOperatorProvider.Except, source1, source2); protected override ShapedQueryExpression TranslateFirstOrDefault(ShapedQueryExpression source, LambdaExpression predicate, Type returnType, bool returnDefault) { @@ -223,7 +223,7 @@ protected override ShapedQueryExpression TranslateGroupJoin(ShapedQueryExpressio => null; protected override ShapedQueryExpression TranslateIntersect(ShapedQueryExpression source1, ShapedQueryExpression source2) - => null; + => TranslateSetOperation(InMemoryLinqOperatorProvider.Intersect, source1, source2); protected override ShapedQueryExpression TranslateJoin(ShapedQueryExpression outer, ShapedQueryExpression inner, LambdaExpression outerKeySelector, LambdaExpression innerKeySelector, LambdaExpression resultSelector) { @@ -582,7 +582,7 @@ protected override ShapedQueryExpression TranslateThenBy(ShapedQueryExpression s } protected override ShapedQueryExpression TranslateUnion(ShapedQueryExpression source1, ShapedQueryExpression source2) - => null; + => TranslateSetOperation(InMemoryLinqOperatorProvider.Union, source1, source2); protected override ShapedQueryExpression TranslateWhere(ShapedQueryExpression source, LambdaExpression predicate) { @@ -681,5 +681,26 @@ private ShapedQueryExpression TranslateSingleResultOperator( return source; } + + private ShapedQueryExpression TranslateSetOperation( + MethodInfo setOperationMethodInfo, + ShapedQueryExpression source1, + ShapedQueryExpression source2) + { + var inMemoryQueryExpression1 = (InMemoryQueryExpression)source1.QueryExpression; + var inMemoryQueryExpression2 = (InMemoryQueryExpression)source2.QueryExpression; + + // Apply any pending selectors, ensuring that the shape of both expressions is identical + // prior to applying the set operation. + inMemoryQueryExpression1.PushdownIntoSubquery(); + inMemoryQueryExpression2.PushdownIntoSubquery(); + + inMemoryQueryExpression1.ServerQueryExpression = Expression.Call( + setOperationMethodInfo.MakeGenericMethod(typeof(ValueBuffer)), + inMemoryQueryExpression1.ServerQueryExpression, + inMemoryQueryExpression2.ServerQueryExpression); + + return source1; + } } } diff --git a/test/EFCore.InMemory.FunctionalTests/Query/SimpleQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/SimpleQueryInMemoryTest.cs index 4c951aac29d..a0d76376ddc 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/SimpleQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/SimpleQueryInMemoryTest.cs @@ -87,171 +87,12 @@ public override void Client_code_using_instance_method_throws() base.Client_code_using_instance_method_throws(); } - #region Set Operations - public override Task Concat(bool isAsync) - { - return Task.CompletedTask; - } - - public override Task Concat_nested(bool isAsync) - { - return Task.CompletedTask; - } - - public override Task Concat_non_entity(bool isAsync) - { - return Task.CompletedTask; - } - - public override Task Except(bool isAsync) - { - return Task.CompletedTask; - } - - public override Task Except_nested(bool isAsync) - { - return Task.CompletedTask; - } - - public override Task Except_non_entity(bool isAsync) - { - return Task.CompletedTask; - } - - public override Task Except_simple_followed_by_projecting_constant(bool isAsync) - { - return Task.CompletedTask; - } - - public override Task Intersect(bool isAsync) - { - return Task.CompletedTask; - } - - public override Task Intersect_nested(bool isAsync) - { - return Task.CompletedTask; - } - - public override Task Intersect_non_entity(bool isAsync) - { - return Task.CompletedTask; - } - - public override Task Union(bool isAsync) - { - return Task.CompletedTask; - } - - public override Task Union_Include(bool isAsync) - { - return Task.CompletedTask; - } - - public override Task Union_Intersect(bool isAsync) - { - return Task.CompletedTask; - } - - public override Task Union_nested(bool isAsync) - { - return Task.CompletedTask; - } - - public override void Union_non_entity(bool isAsync) - { - } - - public override Task Union_OrderBy_Skip_Take(bool isAsync) - { - return Task.CompletedTask; - } - - public override Task Union_over_different_projection_types(bool isAsync, string leftType, string rightType) - { - return Task.CompletedTask; - } - - public override Task Union_Select(bool isAsync) - { - return Task.CompletedTask; - } - - public override Task Union_Skip_Take_OrderBy_ThenBy_Where(bool isAsync) - { - return Task.CompletedTask; - } - - public override Task Union_Take_Union_Take(bool isAsync) - { - return Task.CompletedTask; - } - - public override Task Union_Union(bool isAsync) - { - return Task.CompletedTask; - } - - public override Task Union_Where(bool isAsync) - { - return Task.CompletedTask; - } - - public override Task Union_with_anonymous_type_projection(bool isAsync) - { - return Task.CompletedTask; - } - - public override Task Include_Union(bool isAsync) - { - return Task.CompletedTask; - } - - public override Task Client_eval_Union_FirstOrDefault(bool isAsync) - { - return Task.CompletedTask; - } - + [ConditionalTheory(Skip = "Issue#16963 (GroupBy)")] public override Task GroupBy_Select_Union(bool isAsync) { return Task.CompletedTask; } - public override void Include_Union_different_includes_throws() - { - } - - public override void Include_Union_only_on_one_side_throws() - { - } - - public override Task Select_Union(bool isAsync) - { - return Task.CompletedTask; - } - - public override Task Select_Union_different_fields_in_anonymous_with_subquery(bool isAsync) - { - return Task.CompletedTask; - } - - public override Task Select_Union_unrelated(bool isAsync) - { - return Task.CompletedTask; - } - - public override Task SubSelect_Union(bool isAsync) - { - return Task.CompletedTask; - } - - public override Task Select_Except_reference_projection(bool isAsync) - { - return Task.CompletedTask; - } - - #endregion - [ConditionalTheory(Skip = "Issue#17386")] public override Task Contains_with_local_tuple_array_closure(bool isAsync) {