diff --git a/src/HotChocolate/Data/src/Data/Sorting/Context/SortingContext.cs b/src/HotChocolate/Data/src/Data/Sorting/Context/SortingContext.cs index 158f96086e0..cb37a662267 100644 --- a/src/HotChocolate/Data/src/Data/Sorting/Context/SortingContext.cs +++ b/src/HotChocolate/Data/src/Data/Sorting/Context/SortingContext.cs @@ -158,11 +158,22 @@ protected override ISyntaxVisitorAction Enter( Context context) { var type = context.Types.Peek(); - var field = (SortField)type.Fields[node.Name.Value]; - var fieldType = field.Type.NamedType(); + if (!type.Fields.TryGetField(node.Name.Value, out var inputField)) + { + context.Parents.Push(null); + return base.Leave(node, context); + } - var parent = context.Parents.Peek(); - context.Parents.Push(Expression.Property(parent, (PropertyInfo)field.Member!)); + var fieldType = inputField.Type.NamedType(); + + if (inputField is SortField field) + { + context.Parents.Push(CreateSelector(context.Parents.Peek(), field.Member)); + } + else + { + context.Parents.Push(null); + } if (fieldType.IsInputObjectType()) { @@ -178,35 +189,49 @@ protected override ISyntaxVisitorAction Leave( { var type = context.Types.Peek(); - if (type.Fields.TryGetField(node.Name.Value, out var inputField) && inputField is SortField sortField) + if (type.Fields.TryGetField(node.Name.Value, out var inputField)) { - var fieldType = sortField.Type.NamedType(); + var fieldType = inputField.Type.NamedType(); var expression = context.Parents.Pop(); if (fieldType.IsInputObjectType()) { context.Types.Pop(); } - else + else if (inputField is SortField && expression is not null) { var ascending = node.Value.Value?.Equals("ASC") ?? true; - context.Completed.Add((expression, ascending, sortField.Member!.GetReturnType())); + context.Completed.Add((expression, ascending, expression.Type)); } } else { - context.Types.Pop(); context.Parents.Pop(); } return base.Leave(node, context); } + private static Expression? CreateSelector(Expression? parent, MemberInfo? member) + { + if (parent is null || member is null) + { + return null; + } + + return member switch + { + PropertyInfo property => Expression.Property(parent, property), + FieldInfo field => Expression.Field(parent, field), + _ => null + }; + } + public class Context { public Stack Types { get; } = new(); - public Stack Parents { get; } = new(); + public Stack Parents { get; } = new(); public List<(Expression, bool, Type)> Completed { get; } = []; } diff --git a/src/HotChocolate/Data/test/Data.Tests/IntegrationTests.cs b/src/HotChocolate/Data/test/Data.Tests/IntegrationTests.cs index 64cb788fa7c..fb6a6dbf547 100644 --- a/src/HotChocolate/Data/test/Data.Tests/IntegrationTests.cs +++ b/src/HotChocolate/Data/test/Data.Tests/IntegrationTests.cs @@ -5,6 +5,7 @@ // ReSharper disable MoveLocalFunctionAfterJumpStatement using GreenDonut.Data; +using HotChocolate.Configuration; using HotChocolate.Data.Filters; using HotChocolate.Data.Sorting; using HotChocolate.Execution; @@ -1067,6 +1068,31 @@ name @include(if: $withName) .Match(); } + [Fact] + public async Task AsSortDefinition_QueryContext_Custom_Field_Without_Member_Does_Not_Fail() + { + // arrange + var executor = await new ServiceCollection() + .AddGraphQL() + .AddSorting() + .AddQueryType() + .BuildRequestExecutorAsync(); + + // act + var result = await executor.ExecuteAsync( + """ + { + customSortBooks(order: [{ metadata: { fieldId: 42, direction: ASC } }]) { + id + title + } + } + """); + + // assert + result.MatchSnapshot(); + } + [QueryType] public static class StaticQuery { @@ -1354,4 +1380,72 @@ public string Name set => _name = value; } } + + public class QueryContextCustomSortQuery + { + [UseSorting(typeof(CustomSortBookSortType))] + public IQueryable GetCustomSortBooks(QueryContext context) + => new[] + { + new CustomSortBook + { + Id = 1, + Title = "Zebra", + Metadata = [] + }, + new CustomSortBook + { + Id = 2, + Title = "Apple", + Metadata = [] + } + }.AsQueryable().With(context); + } + + public class CustomSortBook + { + public int Id { get; set; } + + public string Title { get; set; } = string.Empty; + + public List Metadata { get; set; } = []; + } + + public class CustomSortBookMetadata + { + public int FieldId { get; set; } + + public string Value { get; set; } = string.Empty; + } + + public class CustomSortMetadataInputType : InputObjectType + { + protected override void Configure(IInputObjectTypeDescriptor descriptor) + { + descriptor.Field("fieldId").Type(); + descriptor.Field("direction").Type(); + } + } + + public class CustomSortFieldHandler : ISortFieldHandler + { + public bool CanHandle( + ITypeCompletionContext context, + ISortInputTypeConfiguration typeConfiguration, + ISortFieldConfiguration fieldConfiguration) + => true; + } + + public class CustomSortBookSortType : SortInputType + { + protected override void Configure(ISortInputTypeDescriptor descriptor) + { + descriptor.BindFieldsExplicitly(); + descriptor.Field(b => b.Title); + descriptor.Field("metadata") + .Type() + .Extend() + .OnBeforeCreate(d => d.Handler = new CustomSortFieldHandler()); + } + } } diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationTests.AsSortDefinition_QueryContext_Custom_Field_Without_Member_Does_Not_Fail.snap b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationTests.AsSortDefinition_QueryContext_Custom_Field_Without_Member_Does_Not_Fail.snap new file mode 100644 index 00000000000..3a1e82e8ece --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationTests.AsSortDefinition_QueryContext_Custom_Field_Without_Member_Does_Not_Fail.snap @@ -0,0 +1,14 @@ +{ + "data": { + "customSortBooks": [ + { + "id": 1, + "title": "Zebra" + }, + { + "id": 2, + "title": "Apple" + } + ] + } +}