-
-
Notifications
You must be signed in to change notification settings - Fork 756
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
ba4eced
commit 1b27143
Showing
39 changed files
with
8,732 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
...ocolate/Data/src/EntityFramework.Helpers/HotChocolate.Data.EntityFramework.Helpers.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<PackageId>HotChocolate.Data.EntityFramework.Helpers</PackageId> | ||
<AssemblyName>HotChocolate.Data.EntityFramework.Helpers</AssemblyName> | ||
<RootNamespace>HotChocolate.Data</RootNamespace> | ||
<Description>Provides helper classes to implement cursor paging in a layerd architecture without the need to reference HotChocolate GraphQL libraries.</Description> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<Using Include="Microsoft.EntityFrameworkCore" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'"> | ||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.0" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'"> | ||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.0" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'"> | ||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<None Include="$(MSBuildThisFileDirectory)..\MSBuild\HotChocolate.Data.props" Pack="true" PackagePath="build/HotChocolate.Data.EntityFramework.props" Visible="false" /> | ||
<None Include="$(MSBuildThisFileDirectory)..\MSBuild\HotChocolate.Data.targets" Pack="true" PackagePath="build/HotChocolate.Data.EntityFramework.targets" Visible="false" /> | ||
</ItemGroup> | ||
|
||
</Project> |
169 changes: 169 additions & 0 deletions
169
src/HotChocolate/Data/src/EntityFramework.Helpers/Paging/BatchQueryRewriter.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
using System.Diagnostics.CodeAnalysis; | ||
using System.Linq.Expressions; | ||
using System.Reflection; | ||
|
||
namespace HotChocolate.Data; | ||
|
||
internal sealed class BatchQueryRewriter<T>(PagingArguments arguments) : ExpressionVisitor | ||
{ | ||
private PropertyInfo? _resultProperty; | ||
private DataSetKey[]? _keys; | ||
|
||
public PropertyInfo ResultProperty => _resultProperty ?? throw new InvalidOperationException(); | ||
|
||
public DataSetKey[] Keys => _keys ?? throw new InvalidOperationException(); | ||
|
||
protected override Expression VisitExtension(Expression node) | ||
=> node.CanReduce | ||
? base.VisitExtension(node) | ||
: node; | ||
|
||
protected override Expression VisitMethodCall(MethodCallExpression node) | ||
{ | ||
if (IsInclude(node) && TryExtractProperty(node, out var property) && _resultProperty is null) | ||
{ | ||
_resultProperty = property; | ||
var newIncludeExpression = RewriteInclude(node, property); | ||
return base.VisitMethodCall(newIncludeExpression); | ||
} | ||
|
||
return base.VisitMethodCall(node); | ||
} | ||
|
||
private MethodCallExpression RewriteInclude(MethodCallExpression node, PropertyInfo property) | ||
{ | ||
var forward = arguments.Last is null; | ||
|
||
var entityType = node.Arguments[0].Type.GetGenericArguments()[0]; | ||
var includeType = property.PropertyType.GetGenericArguments()[0]; | ||
var lambda = (LambdaExpression)((UnaryExpression)node.Arguments[1]).Operand; | ||
|
||
var parser = new DataSetKeyParser(); | ||
parser.Visit(lambda); | ||
var keys = _keys = parser.Keys.ToArray(); | ||
|
||
var pagingExpr = ApplyPaging(lambda.Body, arguments, keys, forward); | ||
var newLambda = Expression.Lambda(pagingExpr, lambda.Parameters); | ||
return Expression.Call(null, Include(), node.Arguments[0], Expression.Constant(newLambda)); | ||
|
||
MethodInfo Include() | ||
=> typeof(EntityFrameworkQueryableExtensions) | ||
.GetMethods(BindingFlags.Public | BindingFlags.Static) | ||
.First(t => t.Name.Equals("Include") && t.GetGenericArguments().Length == 2) | ||
.MakeGenericMethod(entityType, typeof(IEnumerable<>).MakeGenericType(includeType)); | ||
} | ||
|
||
private static Expression ApplyPaging( | ||
Expression enumerable, | ||
PagingArguments pagingArgs, | ||
DataSetKey[] keys, | ||
bool forward) | ||
{ | ||
MethodInfo? where = null; | ||
MethodInfo? take = null; | ||
|
||
if (pagingArgs.After is not null) | ||
{ | ||
var cursor = CursorParser.Parse(pagingArgs.After, keys); | ||
enumerable = Expression.Call( | ||
null, | ||
Where(), | ||
enumerable, | ||
PagingQueryableExtensions.BuildWhereExpression<T>(keys, cursor, forward)); | ||
} | ||
|
||
if (pagingArgs.Before is not null) | ||
{ | ||
var cursor = CursorParser.Parse(pagingArgs.Before, keys); | ||
enumerable = Expression.Call( | ||
null, | ||
Where(), | ||
enumerable, | ||
PagingQueryableExtensions.BuildWhereExpression<T>(keys, cursor, forward)); | ||
} | ||
|
||
if (pagingArgs.First is not null) | ||
{ | ||
var first = Expression.Constant(pagingArgs.First.Value); | ||
enumerable = Expression.Call(null, Take(), enumerable, first); | ||
} | ||
|
||
if (pagingArgs.Last is not null) | ||
{ | ||
var last = Expression.Constant(pagingArgs.Last.Value); | ||
enumerable = Expression.Call(null, Take(), enumerable, last); | ||
} | ||
|
||
return enumerable; | ||
|
||
MethodInfo Where() | ||
=> where ??= typeof(Enumerable) | ||
.GetMethods(BindingFlags.Public | BindingFlags.Static) | ||
.First(t => t.Name.Equals("Where") && t.GetGenericArguments().Length == 1) | ||
.MakeGenericMethod(typeof(T)); | ||
|
||
MethodInfo Take() | ||
=> take ??= typeof(Enumerable) | ||
.GetMethods(BindingFlags.Public | BindingFlags.Static) | ||
.First(t => t.Name.Equals("Take") && t.GetGenericArguments().Length == 1) | ||
.MakeGenericMethod(typeof(T)); | ||
} | ||
|
||
private static bool IsInclude(MethodCallExpression node) | ||
=> IsMethod(node, nameof(EntityFrameworkQueryableExtensions.Include)); | ||
|
||
private static bool IsMethod(MethodCallExpression node, string name) | ||
=> node.Method.DeclaringType == typeof(EntityFrameworkQueryableExtensions) && | ||
node.Method.Name.Equals(name, StringComparison.Ordinal); | ||
|
||
private static bool TryExtractProperty( | ||
MethodCallExpression node, | ||
[NotNullWhen(true)] out PropertyInfo? property) | ||
{ | ||
if (node.Arguments is [_, UnaryExpression { Operand: LambdaExpression l }]) | ||
{ | ||
return TryExtractProperty1(l.Body, out property); | ||
} | ||
|
||
property = null; | ||
return false; | ||
} | ||
|
||
private static bool TryExtractProperty1(Expression expression, out PropertyInfo? property) | ||
{ | ||
property = null; | ||
|
||
switch (expression) | ||
{ | ||
case MemberExpression memberExpression: | ||
property = memberExpression.Member as PropertyInfo; | ||
return property != null; | ||
|
||
case MethodCallExpression methodCallExpression: | ||
{ | ||
if (methodCallExpression.Arguments.Count > 0) | ||
{ | ||
var firstArgument = methodCallExpression.Arguments[0]; | ||
|
||
switch (firstArgument) | ||
{ | ||
case MethodCallExpression: | ||
return TryExtractProperty1(firstArgument, out property); | ||
|
||
case UnaryExpression unaryExpression: | ||
return TryExtractProperty1(unaryExpression.Operand, out property); | ||
|
||
case MemberExpression: | ||
return TryExtractProperty1(firstArgument, out property); | ||
} | ||
} | ||
break; | ||
} | ||
|
||
case UnaryExpression unaryExpression: | ||
return TryExtractProperty1(unaryExpression.Operand, out property); | ||
} | ||
|
||
return false; | ||
} | ||
} |
Oops, something went wrong.