diff --git a/src/EFCore/EntityFrameworkQueryableExtensions.cs b/src/EFCore/EntityFrameworkQueryableExtensions.cs index 44dc48660b3..9d7cbe0484e 100644 --- a/src/EFCore/EntityFrameworkQueryableExtensions.cs +++ b/src/EFCore/EntityFrameworkQueryableExtensions.cs @@ -690,6 +690,103 @@ public static Task SingleOrDefaultAsync( #endregion + #region TakeAfterMatch + + internal static readonly MethodInfo TakeAfterMatchMethodInfo + = typeof(EntityFrameworkQueryableExtensions) + .GetTypeInfo().GetDeclaredMethod(nameof(TakeAfterMatch)); + + /// + /// Returns a new query where the results will the next + /// elements after the first element that matches . + /// + /// + /// The type of the elements of . + /// + /// + /// An to return results from. + /// + /// + /// An function to test an element for the first match. + /// + /// + /// The number of elements to return. + /// + /// + /// A new query where the result set will be the next + /// elements after the first element that matches . + /// + public static IQueryable TakeAfterMatch( + [NotNull] this IQueryable source, + [NotNull] Expression> predicate, + [NotNull] int count) + where TSource : class + { + Check.NotNull(source, nameof(source)); + Check.NotNull(predicate, nameof(predicate)); + Check.NotNull(count, nameof(count)); + + return + source.Provider is EntityQueryProvider + ? source.Provider.CreateQuery( + Expression.Call( + instance: null, + method: TakeAfterMatchMethodInfo.MakeGenericMethod(typeof(TSource)), + arguments: new[] { source.Expression, predicate, Expression.Constant(count, typeof(int)) })) + : source; + } + + #endregion + + #region TakeBeforeMatch + + internal static readonly MethodInfo TakeBeforeMatchMethodInfo + = typeof(EntityFrameworkQueryableExtensions) + .GetTypeInfo().GetDeclaredMethod(nameof(TakeBeforeMatch)); + + /// + /// Returns a new query where the results will the previous + /// elements before the first element that matches . + /// + /// + /// The type of the elements of . + /// + /// + /// An to return results from. + /// + /// + /// An function to test an element for the first match. + /// + /// + /// The number of elements to return. + /// + /// + /// A new query where the result set will be the previous + /// elements before the first element that matches . + /// + public static IQueryable TakeBeforeMatch( + [NotNull] this IQueryable source, + [NotNull] Expression> predicate, + [NotNull] int count) + where TSource : class + { + Check.NotNull(source, nameof(source)); + Check.NotNull(predicate, nameof(predicate)); + Check.NotNull(count, nameof(count)); + + return + source.Provider is EntityQueryProvider + ? source.Provider.CreateQuery( + Expression.Call( + instance: null, + method: TakeBeforeMatchMethodInfo.MakeGenericMethod(typeof(TSource)), + arguments: new[] { source.Expression, predicate, Expression.Constant(count, typeof(int)) })) + : source; + } + + #endregion + + #region Min private static readonly MethodInfo _min = GetMethod(nameof(Queryable.Min), predicate: mi => mi.IsGenericMethod); diff --git a/src/EFCore/Query/Internal/MethodInfoBasedNodeTypeRegistryFactory.cs b/src/EFCore/Query/Internal/MethodInfoBasedNodeTypeRegistryFactory.cs index 94b0c13f0b8..9b283f1997c 100644 --- a/src/EFCore/Query/Internal/MethodInfoBasedNodeTypeRegistryFactory.cs +++ b/src/EFCore/Query/Internal/MethodInfoBasedNodeTypeRegistryFactory.cs @@ -58,6 +58,8 @@ public MethodInfoBasedNodeTypeRegistryFactory( .Register(StringIncludeExpressionNode.SupportedMethods, typeof(StringIncludeExpressionNode)); _methodInfoBasedNodeTypeRegistry .Register(ThenIncludeExpressionNode.SupportedMethods, typeof(ThenIncludeExpressionNode)); + _methodInfoBasedNodeTypeRegistry + .Register(TakeBesideMatchExpressionNode.SupportedMethods, typeof(TakeBesideMatchExpressionNode)); _nodeTypeProviders = new INodeTypeProvider[] { _methodInfoBasedNodeTypeRegistry, _methodNameBasedNodeTypeRegistry }; } diff --git a/src/EFCore/Query/ResultOperators/Internal/IncludeResultOperator.cs b/src/EFCore/Query/ResultOperators/Internal/IncludeResultOperator.cs index e5a7c9bf78b..a708433f750 100644 --- a/src/EFCore/Query/ResultOperators/Internal/IncludeResultOperator.cs +++ b/src/EFCore/Query/ResultOperators/Internal/IncludeResultOperator.cs @@ -33,7 +33,9 @@ public class IncludeResultOperator : SequenceTypePreservingResultOperatorBase, I /// directly from your code. This API may change or be removed in future releases. /// public IncludeResultOperator( - [NotNull] INavigation[] navigationPath, [NotNull] Expression pathFromQuerySource, bool implicitLoad = false) + [NotNull] INavigation[] navigationPath, + [NotNull] Expression pathFromQuerySource, + bool implicitLoad = false) { _navigationPaths = new List { diff --git a/src/EFCore/Query/ResultOperators/Internal/TakeBesideMatchExpressionNode.cs b/src/EFCore/Query/ResultOperators/Internal/TakeBesideMatchExpressionNode.cs new file mode 100644 index 00000000000..ab3680df68a --- /dev/null +++ b/src/EFCore/Query/ResultOperators/Internal/TakeBesideMatchExpressionNode.cs @@ -0,0 +1,70 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; +using JetBrains.Annotations; +using Remotion.Linq.Clauses; +using Remotion.Linq.Parsing.Structure.IntermediateModel; + +namespace Microsoft.EntityFrameworkCore.Query.ResultOperators.Internal +{ + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class TakeBesideMatchExpressionNode : ResultOperatorExpressionNodeBase + { + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static readonly IReadOnlyCollection SupportedMethods = new[] + { + EntityFrameworkQueryableExtensions.TakeAfterMatchMethodInfo, + EntityFrameworkQueryableExtensions.TakeBeforeMatchMethodInfo + }; + + private readonly LambdaExpression _lambdaExpression; + + private readonly Expression _count; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public TakeBesideMatchExpressionNode( + MethodCallExpressionParseInfo parseInfo, + [NotNull] LambdaExpression predicateLambda, + [NotNull] Expression count) + : base(parseInfo, predicateLambda, null) + { + _count = count; + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + protected override ResultOperatorBase CreateResultOperator(ClauseGenerationContext clauseGenerationContext) + { + var takeBesideMatchOperator = new TakeBesideMatchResultOperator( + forward: ParsedExpression.Method.GetGenericMethodDefinition() + .Equals(EntityFrameworkQueryableExtensions.TakeAfterMatchMethodInfo) + ); + // etc... + return takeBesideMatchOperator; + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public override Expression Resolve( + ParameterExpression inputParameter, + Expression expressionToBeResolved, + ClauseGenerationContext clauseGenerationContext) + => Source.Resolve(inputParameter, expressionToBeResolved, clauseGenerationContext); + } +} diff --git a/src/EFCore/Query/ResultOperators/Internal/TakeBesideMatchResultOperator.cs b/src/EFCore/Query/ResultOperators/Internal/TakeBesideMatchResultOperator.cs new file mode 100644 index 00000000000..853fdcefdb6 --- /dev/null +++ b/src/EFCore/Query/ResultOperators/Internal/TakeBesideMatchResultOperator.cs @@ -0,0 +1,106 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq.Expressions; +using JetBrains.Annotations; +using Remotion.Linq; +using Remotion.Linq.Clauses; +using Remotion.Linq.Clauses.ResultOperators; +using Remotion.Linq.Clauses.StreamedData; + +namespace Microsoft.EntityFrameworkCore.Query.ResultOperators.Internal +{ + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class TakeBesideMatchResultOperator : SequenceTypePreservingResultOperatorBase, IQueryAnnotation + { + private Expression _count; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public TakeBesideMatchResultOperator(bool forward, LambdaExpression predicate, Expression count) + { + IsForward = forward; + + Count = count; + } + + + + public Expression Count + { + get { return _count; } + set + { + //ArgumentUtility.CheckNotNull("value", value); + if (value.Type != typeof(int)) + { + var message = string.Format("The value expression returns '{0}', an expression returning 'System.Int32' was expected.", value.Type); + throw new ArgumentException(message, "value"); + } + + _count = value; + } + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual IQuerySource QuerySource { get; [NotNull] set; } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual QueryModel QueryModel { get; [NotNull] set; } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual bool IsForward { get; } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public override string ToString() + { + var paramsString = $"TODO,Count.BuildString()}"; + return IsForward + ? $"TakeAfterMatch({paramsString})" + : $"TakeBeforeMatch({paramsString})"; + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public override ResultOperatorBase Clone(CloneContext cloneContext) + => new TakeBesideMatchResultOperator(IsForward, ); + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public override void TransformExpressions(Func transformation) + { + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public override StreamedSequence ExecuteInMemory(StreamedSequence input) + { + var sequence = input.GetTypedSequence(); + var match = sequence.FirstOrDefault() // TODO: Should use SingleOrDefault()? + } + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/CursorBasedPagingTest.cs b/test/EFCore.SqlServer.FunctionalTests/CursorBasedPagingTest.cs new file mode 100644 index 00000000000..05f5e112ab5 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/CursorBasedPagingTest.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.TestModels.GearsOfWarModel; +using Microsoft.EntityFrameworkCore.TestUtilities.Xunit; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.EntityFrameworkCore.Query +{ + public class CursorBasedPagingTest : QueryTestBase, IDisposable + { + public CursorBasedPagingTest(GearsOfWarQuerySqlServerFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public void Dispose() + { + + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task TakeAfterMatch(bool isAsync) + { + await AssertQuery( + isAsync, + gs => gs.TakeAfterMatch(g => g.IsMarcus, 5)); + + AssertSql( + @"xyz"); + } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + } +}