Skip to content

Commit

Permalink
Added Paging Helpers (#6935)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelstaib authored Feb 23, 2024
1 parent ba4eced commit 1b27143
Show file tree
Hide file tree
Showing 39 changed files with 8,732 additions and 29 deletions.
4 changes: 3 additions & 1 deletion cSpell.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@
"meros",
"Structs",
"reencode",
"WunderGraph"
"WunderGraph",
"CCPA",
"decompile"
],
"ignoreWords": [
"Badurina",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ public static void MatchSnapshot(
ISnapshotValueFormatter? formatter = null)
=> Snapshot.Match(value, postFix?.ToString(), extension, formatter);

public static void MatchMarkdownSnapshot(
this object? value,
object? postFix = null,
string? extension = null,
ISnapshotValueFormatter? formatter = null)
=> Snapshot.Create(postFix?.ToString(), extension).Add(value, formatter: formatter).MatchMarkdown();

public static void MatchSnapshot(
this ISyntaxNode? value,
string? postFix = null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace CookieCrumble.Formatters;

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

public bool CanHandle(object? value)
=> true;

public void Format(IBufferWriter<byte> snapshot, object? value)
=> snapshot.Append(JsonConvert.SerializeObject(value, _settings));

public void FormatMarkdown(IBufferWriter<byte> snapshot, object? value)
{
snapshot.Append("```json");
snapshot.AppendLine();
Format(snapshot, value);
snapshot.AppendLine();
snapshot.Append("```");
snapshot.AppendLine();
}

private class ChildFirstContractResolver : DefaultContractResolver
{
Expand Down
2 changes: 2 additions & 0 deletions src/HotChocolate/Data/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
<WarningsAsErrors>$(WarningsAsErrors);nullable</WarningsAsErrors>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<NeutralLanguage>en</NeutralLanguage>
<ImplicitUsings>enable</ImplicitUsings>

</PropertyGroup>

</Project>
15 changes: 15 additions & 0 deletions src/HotChocolate/Data/HotChocolate.Data.sln
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Data.AutoMappe
EndProject
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}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Data.EntityFramework.Helpers", "src\EntityFramework.Helpers\HotChocolate.Data.EntityFramework.Helpers.csproj", "{F781C048-BCA9-4560-B796-4E892088E1BA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -133,6 +135,7 @@ Global
{0AB70663-9D52-4415-B265-0D1F001D7576} = {91887A91-7B1C-4287-A1E0-BD4E0DAF24C7}
{F793AC13-0500-492A-914D-4229F6AE0687} = {4EE990B2-C327-46DA-8FE8-F95AC228E47F}
{AB5D66E9-FA86-4AAE-910A-BEEC1C4B8A80} = {882EC02D-5E1D-41F5-AD9F-AA06E31D133A}
{F781C048-BCA9-4560-B796-4E892088E1BA} = {91887A91-7B1C-4287-A1E0-BD4E0DAF24C7}
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{D68A0AB9-871A-487B-8D12-1A7544D81B9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
Expand Down Expand Up @@ -545,5 +548,17 @@ Global
{AB5D66E9-FA86-4AAE-910A-BEEC1C4B8A80}.Release|x64.Build.0 = Release|Any CPU
{AB5D66E9-FA86-4AAE-910A-BEEC1C4B8A80}.Release|x86.ActiveCfg = Release|Any CPU
{AB5D66E9-FA86-4AAE-910A-BEEC1C4B8A80}.Release|x86.Build.0 = Release|Any CPU
{F781C048-BCA9-4560-B796-4E892088E1BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F781C048-BCA9-4560-B796-4E892088E1BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F781C048-BCA9-4560-B796-4E892088E1BA}.Debug|x64.ActiveCfg = Debug|Any CPU
{F781C048-BCA9-4560-B796-4E892088E1BA}.Debug|x64.Build.0 = Debug|Any CPU
{F781C048-BCA9-4560-B796-4E892088E1BA}.Debug|x86.ActiveCfg = Debug|Any CPU
{F781C048-BCA9-4560-B796-4E892088E1BA}.Debug|x86.Build.0 = Debug|Any CPU
{F781C048-BCA9-4560-B796-4E892088E1BA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F781C048-BCA9-4560-B796-4E892088E1BA}.Release|Any CPU.Build.0 = Release|Any CPU
{F781C048-BCA9-4560-B796-4E892088E1BA}.Release|x64.ActiveCfg = Release|Any CPU
{F781C048-BCA9-4560-B796-4E892088E1BA}.Release|x64.Build.0 = Release|Any CPU
{F781C048-BCA9-4560-B796-4E892088E1BA}.Release|x86.ActiveCfg = Release|Any CPU
{F781C048-BCA9-4560-B796-4E892088E1BA}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
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>
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;
}
}
Loading

0 comments on commit 1b27143

Please sign in to comment.