Skip to content

Commit 1b27143

Browse files
authored
Added Paging Helpers (#6935)
1 parent ba4eced commit 1b27143

39 files changed

+8732
-29
lines changed

cSpell.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@
4343
"meros",
4444
"Structs",
4545
"reencode",
46-
"WunderGraph"
46+
"WunderGraph",
47+
"CCPA",
48+
"decompile"
4749
],
4850
"ignoreWords": [
4951
"Badurina",

src/CookieCrumble/src/CookieCrumble/Extensions/SnapshotExtensions.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ public static void MatchSnapshot(
2323
ISnapshotValueFormatter? formatter = null)
2424
=> Snapshot.Match(value, postFix?.ToString(), extension, formatter);
2525

26+
public static void MatchMarkdownSnapshot(
27+
this object? value,
28+
object? postFix = null,
29+
string? extension = null,
30+
ISnapshotValueFormatter? formatter = null)
31+
=> Snapshot.Create(postFix?.ToString(), extension).Add(value, formatter: formatter).MatchMarkdown();
32+
2633
public static void MatchSnapshot(
2734
this ISyntaxNode? value,
2835
string? postFix = null)

src/CookieCrumble/src/CookieCrumble/Formatters/JsonSnapshotValueFormatter.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
namespace CookieCrumble.Formatters;
88

9-
internal sealed class JsonSnapshotValueFormatter : ISnapshotValueFormatter
9+
internal sealed class JsonSnapshotValueFormatter : ISnapshotValueFormatter, IMarkdownSnapshotValueFormatter
1010
{
1111
private static readonly JsonSerializerSettings _settings =
1212
new()
@@ -22,9 +22,19 @@ internal sealed class JsonSnapshotValueFormatter : ISnapshotValueFormatter
2222

2323
public bool CanHandle(object? value)
2424
=> true;
25-
25+
2626
public void Format(IBufferWriter<byte> snapshot, object? value)
2727
=> snapshot.Append(JsonConvert.SerializeObject(value, _settings));
28+
29+
public void FormatMarkdown(IBufferWriter<byte> snapshot, object? value)
30+
{
31+
snapshot.Append("```json");
32+
snapshot.AppendLine();
33+
Format(snapshot, value);
34+
snapshot.AppendLine();
35+
snapshot.Append("```");
36+
snapshot.AppendLine();
37+
}
2838

2939
private class ChildFirstContractResolver : DefaultContractResolver
3040
{

src/HotChocolate/Data/Directory.Build.props

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
<WarningsAsErrors>$(WarningsAsErrors);nullable</WarningsAsErrors>
99
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
1010
<NeutralLanguage>en</NeutralLanguage>
11+
<ImplicitUsings>enable</ImplicitUsings>
12+
1113
</PropertyGroup>
1214

1315
</Project>

src/HotChocolate/Data/HotChocolate.Data.sln

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Data.AutoMappe
8585
EndProject
8686
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.AspNetCore.Tests.Utilities", "..\AspNetCore\test\AspNetCore.Tests.Utilities\HotChocolate.AspNetCore.Tests.Utilities.csproj", "{AB5D66E9-FA86-4AAE-910A-BEEC1C4B8A80}"
8787
EndProject
88+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Data.EntityFramework.Helpers", "src\EntityFramework.Helpers\HotChocolate.Data.EntityFramework.Helpers.csproj", "{F781C048-BCA9-4560-B796-4E892088E1BA}"
89+
EndProject
8890
Global
8991
GlobalSection(SolutionConfigurationPlatforms) = preSolution
9092
Debug|Any CPU = Debug|Any CPU
@@ -133,6 +135,7 @@ Global
133135
{0AB70663-9D52-4415-B265-0D1F001D7576} = {91887A91-7B1C-4287-A1E0-BD4E0DAF24C7}
134136
{F793AC13-0500-492A-914D-4229F6AE0687} = {4EE990B2-C327-46DA-8FE8-F95AC228E47F}
135137
{AB5D66E9-FA86-4AAE-910A-BEEC1C4B8A80} = {882EC02D-5E1D-41F5-AD9F-AA06E31D133A}
138+
{F781C048-BCA9-4560-B796-4E892088E1BA} = {91887A91-7B1C-4287-A1E0-BD4E0DAF24C7}
136139
EndGlobalSection
137140
GlobalSection(ProjectConfigurationPlatforms) = postSolution
138141
{D68A0AB9-871A-487B-8D12-1A7544D81B9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@@ -545,5 +548,17 @@ Global
545548
{AB5D66E9-FA86-4AAE-910A-BEEC1C4B8A80}.Release|x64.Build.0 = Release|Any CPU
546549
{AB5D66E9-FA86-4AAE-910A-BEEC1C4B8A80}.Release|x86.ActiveCfg = Release|Any CPU
547550
{AB5D66E9-FA86-4AAE-910A-BEEC1C4B8A80}.Release|x86.Build.0 = Release|Any CPU
551+
{F781C048-BCA9-4560-B796-4E892088E1BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
552+
{F781C048-BCA9-4560-B796-4E892088E1BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
553+
{F781C048-BCA9-4560-B796-4E892088E1BA}.Debug|x64.ActiveCfg = Debug|Any CPU
554+
{F781C048-BCA9-4560-B796-4E892088E1BA}.Debug|x64.Build.0 = Debug|Any CPU
555+
{F781C048-BCA9-4560-B796-4E892088E1BA}.Debug|x86.ActiveCfg = Debug|Any CPU
556+
{F781C048-BCA9-4560-B796-4E892088E1BA}.Debug|x86.Build.0 = Debug|Any CPU
557+
{F781C048-BCA9-4560-B796-4E892088E1BA}.Release|Any CPU.ActiveCfg = Release|Any CPU
558+
{F781C048-BCA9-4560-B796-4E892088E1BA}.Release|Any CPU.Build.0 = Release|Any CPU
559+
{F781C048-BCA9-4560-B796-4E892088E1BA}.Release|x64.ActiveCfg = Release|Any CPU
560+
{F781C048-BCA9-4560-B796-4E892088E1BA}.Release|x64.Build.0 = Release|Any CPU
561+
{F781C048-BCA9-4560-B796-4E892088E1BA}.Release|x86.ActiveCfg = Release|Any CPU
562+
{F781C048-BCA9-4560-B796-4E892088E1BA}.Release|x86.Build.0 = Release|Any CPU
548563
EndGlobalSection
549564
EndGlobal
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<PackageId>HotChocolate.Data.EntityFramework.Helpers</PackageId>
5+
<AssemblyName>HotChocolate.Data.EntityFramework.Helpers</AssemblyName>
6+
<RootNamespace>HotChocolate.Data</RootNamespace>
7+
<Description>Provides helper classes to implement cursor paging in a layerd architecture without the need to reference HotChocolate GraphQL libraries.</Description>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<Using Include="Microsoft.EntityFrameworkCore" />
12+
</ItemGroup>
13+
14+
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
15+
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.0" />
16+
</ItemGroup>
17+
18+
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
19+
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.0" />
20+
</ItemGroup>
21+
22+
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
23+
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
24+
</ItemGroup>
25+
26+
<ItemGroup>
27+
<None Include="$(MSBuildThisFileDirectory)..\MSBuild\HotChocolate.Data.props" Pack="true" PackagePath="build/HotChocolate.Data.EntityFramework.props" Visible="false" />
28+
<None Include="$(MSBuildThisFileDirectory)..\MSBuild\HotChocolate.Data.targets" Pack="true" PackagePath="build/HotChocolate.Data.EntityFramework.targets" Visible="false" />
29+
</ItemGroup>
30+
31+
</Project>
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using System.Linq.Expressions;
3+
using System.Reflection;
4+
5+
namespace HotChocolate.Data;
6+
7+
internal sealed class BatchQueryRewriter<T>(PagingArguments arguments) : ExpressionVisitor
8+
{
9+
private PropertyInfo? _resultProperty;
10+
private DataSetKey[]? _keys;
11+
12+
public PropertyInfo ResultProperty => _resultProperty ?? throw new InvalidOperationException();
13+
14+
public DataSetKey[] Keys => _keys ?? throw new InvalidOperationException();
15+
16+
protected override Expression VisitExtension(Expression node)
17+
=> node.CanReduce
18+
? base.VisitExtension(node)
19+
: node;
20+
21+
protected override Expression VisitMethodCall(MethodCallExpression node)
22+
{
23+
if (IsInclude(node) && TryExtractProperty(node, out var property) && _resultProperty is null)
24+
{
25+
_resultProperty = property;
26+
var newIncludeExpression = RewriteInclude(node, property);
27+
return base.VisitMethodCall(newIncludeExpression);
28+
}
29+
30+
return base.VisitMethodCall(node);
31+
}
32+
33+
private MethodCallExpression RewriteInclude(MethodCallExpression node, PropertyInfo property)
34+
{
35+
var forward = arguments.Last is null;
36+
37+
var entityType = node.Arguments[0].Type.GetGenericArguments()[0];
38+
var includeType = property.PropertyType.GetGenericArguments()[0];
39+
var lambda = (LambdaExpression)((UnaryExpression)node.Arguments[1]).Operand;
40+
41+
var parser = new DataSetKeyParser();
42+
parser.Visit(lambda);
43+
var keys = _keys = parser.Keys.ToArray();
44+
45+
var pagingExpr = ApplyPaging(lambda.Body, arguments, keys, forward);
46+
var newLambda = Expression.Lambda(pagingExpr, lambda.Parameters);
47+
return Expression.Call(null, Include(), node.Arguments[0], Expression.Constant(newLambda));
48+
49+
MethodInfo Include()
50+
=> typeof(EntityFrameworkQueryableExtensions)
51+
.GetMethods(BindingFlags.Public | BindingFlags.Static)
52+
.First(t => t.Name.Equals("Include") && t.GetGenericArguments().Length == 2)
53+
.MakeGenericMethod(entityType, typeof(IEnumerable<>).MakeGenericType(includeType));
54+
}
55+
56+
private static Expression ApplyPaging(
57+
Expression enumerable,
58+
PagingArguments pagingArgs,
59+
DataSetKey[] keys,
60+
bool forward)
61+
{
62+
MethodInfo? where = null;
63+
MethodInfo? take = null;
64+
65+
if (pagingArgs.After is not null)
66+
{
67+
var cursor = CursorParser.Parse(pagingArgs.After, keys);
68+
enumerable = Expression.Call(
69+
null,
70+
Where(),
71+
enumerable,
72+
PagingQueryableExtensions.BuildWhereExpression<T>(keys, cursor, forward));
73+
}
74+
75+
if (pagingArgs.Before is not null)
76+
{
77+
var cursor = CursorParser.Parse(pagingArgs.Before, keys);
78+
enumerable = Expression.Call(
79+
null,
80+
Where(),
81+
enumerable,
82+
PagingQueryableExtensions.BuildWhereExpression<T>(keys, cursor, forward));
83+
}
84+
85+
if (pagingArgs.First is not null)
86+
{
87+
var first = Expression.Constant(pagingArgs.First.Value);
88+
enumerable = Expression.Call(null, Take(), enumerable, first);
89+
}
90+
91+
if (pagingArgs.Last is not null)
92+
{
93+
var last = Expression.Constant(pagingArgs.Last.Value);
94+
enumerable = Expression.Call(null, Take(), enumerable, last);
95+
}
96+
97+
return enumerable;
98+
99+
MethodInfo Where()
100+
=> where ??= typeof(Enumerable)
101+
.GetMethods(BindingFlags.Public | BindingFlags.Static)
102+
.First(t => t.Name.Equals("Where") && t.GetGenericArguments().Length == 1)
103+
.MakeGenericMethod(typeof(T));
104+
105+
MethodInfo Take()
106+
=> take ??= typeof(Enumerable)
107+
.GetMethods(BindingFlags.Public | BindingFlags.Static)
108+
.First(t => t.Name.Equals("Take") && t.GetGenericArguments().Length == 1)
109+
.MakeGenericMethod(typeof(T));
110+
}
111+
112+
private static bool IsInclude(MethodCallExpression node)
113+
=> IsMethod(node, nameof(EntityFrameworkQueryableExtensions.Include));
114+
115+
private static bool IsMethod(MethodCallExpression node, string name)
116+
=> node.Method.DeclaringType == typeof(EntityFrameworkQueryableExtensions) &&
117+
node.Method.Name.Equals(name, StringComparison.Ordinal);
118+
119+
private static bool TryExtractProperty(
120+
MethodCallExpression node,
121+
[NotNullWhen(true)] out PropertyInfo? property)
122+
{
123+
if (node.Arguments is [_, UnaryExpression { Operand: LambdaExpression l }])
124+
{
125+
return TryExtractProperty1(l.Body, out property);
126+
}
127+
128+
property = null;
129+
return false;
130+
}
131+
132+
private static bool TryExtractProperty1(Expression expression, out PropertyInfo? property)
133+
{
134+
property = null;
135+
136+
switch (expression)
137+
{
138+
case MemberExpression memberExpression:
139+
property = memberExpression.Member as PropertyInfo;
140+
return property != null;
141+
142+
case MethodCallExpression methodCallExpression:
143+
{
144+
if (methodCallExpression.Arguments.Count > 0)
145+
{
146+
var firstArgument = methodCallExpression.Arguments[0];
147+
148+
switch (firstArgument)
149+
{
150+
case MethodCallExpression:
151+
return TryExtractProperty1(firstArgument, out property);
152+
153+
case UnaryExpression unaryExpression:
154+
return TryExtractProperty1(unaryExpression.Operand, out property);
155+
156+
case MemberExpression:
157+
return TryExtractProperty1(firstArgument, out property);
158+
}
159+
}
160+
break;
161+
}
162+
163+
case UnaryExpression unaryExpression:
164+
return TryExtractProperty1(unaryExpression.Operand, out property);
165+
}
166+
167+
return false;
168+
}
169+
}

0 commit comments

Comments
 (0)