Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Paging Helpers #6935

Merged
merged 6 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading