Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ private void CollectTypes(Context context, Selection selection, TypeContainer pa
return BuildSelectionSetExpression(context, singleTypeNode);
}

private static MemberInitExpression? BuildSelectionSetExpression(
private static Expression? BuildSelectionSetExpression(
Context context,
TypeNode parent)
{
Expand All @@ -210,9 +210,69 @@ private void CollectTypes(Context context, Selection selection, TypeContainer pa
return null;
}

return Expression.MemberInit(
Expression.New(context.ParentType),
assignments.ToImmutable());
var assignmentList = assignments.ToImmutable();

// Preferred path for mutable types.
var parameterlessConstructor = context.ParentType.GetConstructor(Type.EmptyTypes);
if (parameterlessConstructor is not null)
{
var allWritable = assignmentList.All(a =>
a.Member is PropertyInfo { CanWrite: true, SetMethod.IsPublic: true });

if (allWritable)
{
return Expression.MemberInit(
Expression.New(parameterlessConstructor),
assignmentList);
}
}

// Fallback path for record-like types without a parameterless constructor.
var bestMatchingConstructor = context.ParentType.GetConstructors()
.Select(c => (Constructor: c, Parameters: c.GetParameters()))
.OrderBy(c => c.Parameters.Length)
.FirstOrDefault(c =>
c.Parameters.Length >= assignmentList.Length
&& assignmentList.All(a =>
c.Parameters.Any(p =>
string.Equals(a.Member.Name, p.Name, StringComparison.OrdinalIgnoreCase)
&& a.Expression.Type.IsAssignableTo(p.ParameterType))));

if (bestMatchingConstructor.Constructor is not null)
{
var arguments = bestMatchingConstructor.Parameters.Select(p =>
{
var assignment = assignmentList.FirstOrDefault(a =>
string.Equals(a.Member.Name, p.Name, StringComparison.OrdinalIgnoreCase)
&& a.Expression.Type.IsAssignableTo(p.ParameterType));

if (assignment is not null)
{
return assignment.Expression.Type == p.ParameterType
? assignment.Expression
: Expression.Convert(assignment.Expression, p.ParameterType);
}

if (p.HasDefaultValue)
{
return Expression.Convert(Expression.Constant(p.DefaultValue), p.ParameterType);
}

if (!p.ParameterType.IsValueType && IsMarkedAsExplicitlyNonNullable(p))
{
throw new InvalidOperationException(
$"Cannot construct '{context.ParentType.Name}': missing required argument '{p.Name}' "
+ "(non-nullable reference type with no default value).");
}

return Expression.Default(p.ParameterType);
}).ToArray();

return Expression.New(bestMatchingConstructor.Constructor, arguments);
}

throw new InvalidOperationException(
$"No writable properties or suitable constructor found for type '{context.ParentType.Name}'.");
}

private void CollectSelection(
Expand Down Expand Up @@ -383,4 +443,7 @@ private readonly record struct Context(
: null;
}
}

private static bool IsMarkedAsExplicitlyNonNullable(ParameterInfo parameter)
=> new NullabilityInfoContext().Create(parameter).WriteState is NullabilityState.NotNull;
}
46 changes: 46 additions & 0 deletions src/HotChocolate/Data/test/Data.Tests/IntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using HotChocolate.Data.Sorting;
using HotChocolate.Execution;
using HotChocolate.Types;
using HotChocolate.Types.Pagination;
using Microsoft.Extensions.DependencyInjection;

namespace HotChocolate.Data;
Expand Down Expand Up @@ -961,6 +962,37 @@ public async Task AsSortDefinition_Descending_QueryContext_2()
result.MatchSnapshot();
}

[Fact]
public async Task QueryContext_Should_Not_Throw_For_Record_Node_With_Paging_Filtering_And_Sorting()
{
// arrange
var executor = await new ServiceCollection()
.AddGraphQL()
.AddFiltering()
.AddSorting()
.AddQueryType<RecordQuery>()
.BuildRequestExecutorAsync();

// act
var result = await executor.ExecuteAsync(
"""
{
users {
edges {
node {
id
firstName
}
}
}
}
""");

// assert
var operationResult = result.ExpectOperationResult();
Assert.Empty(operationResult.Errors);
}

[Fact]
public async Task QueryContext_Selector_Respects_Include_Directive()
{
Expand Down Expand Up @@ -1364,6 +1396,20 @@ public IQueryable<ConditionalAuthor> GetConditionalAuthors(
.With(context);
}

public class RecordQuery
{
[UsePaging]
[UseFiltering]
[UseSorting]
public Connection<UserRecord> GetUsers(
QueryContext<UserRecord> query)
=> Connection.Empty<UserRecord>();

public record UserRecord(
string Id,
string FirstName);
}

public sealed class ConditionalAuthor
{
private string _name = "author";
Expand Down
Loading