diff --git a/src/HotChocolate/Core/src/Execution.Projections/SelectionExpressionBuilder.cs b/src/HotChocolate/Core/src/Execution.Projections/SelectionExpressionBuilder.cs index ba2f65f3810..15f889d8b88 100644 --- a/src/HotChocolate/Core/src/Execution.Projections/SelectionExpressionBuilder.cs +++ b/src/HotChocolate/Core/src/Execution.Projections/SelectionExpressionBuilder.cs @@ -632,5 +632,15 @@ private readonly record struct Context( private static bool ShouldReuseExistingInstance(Type type) => type.GetConstructor(Type.EmptyTypes) is not null && type.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic) - .Any(t => t.GetParameters().Length > 0); + .Any(t => + t.GetParameters().Length > 0 + && !IsRecordCopyConstructor(t, type)); + + private static bool IsRecordCopyConstructor(ConstructorInfo constructor, Type declaringType) + { + var parameters = constructor.GetParameters(); + + return parameters.Length == 1 + && parameters[0].ParameterType == declaringType; + } } diff --git a/src/HotChocolate/Data/test/Data.Tests/IntegrationTests.cs b/src/HotChocolate/Data/test/Data.Tests/IntegrationTests.cs index bb9deaebf3f..340eb230ceb 100644 --- a/src/HotChocolate/Data/test/Data.Tests/IntegrationTests.cs +++ b/src/HotChocolate/Data/test/Data.Tests/IntegrationTests.cs @@ -1100,6 +1100,38 @@ name @include(if: $withName) .Match(); } + [Fact] + public async Task QueryContext_Selector_For_Record_With_Init_Properties_Should_Not_Be_Identity() + { + // arrange + var capture = new QueryContextRecordSelectorCapture(); + var executor = await new ServiceCollection() + .AddSingleton(capture) + .AddGraphQL() + .AddQueryContext() + .AddFiltering() + .AddSorting() + .AddQueryType() + .BuildRequestExecutorAsync(); + + // act + var result = await executor.ExecuteAsync( + """ + { + recordSelector { + id + } + } + """); + + // assert + var operationResult = result.ExpectOperationResult(); + Assert.Empty(operationResult.Errors); + Assert.False( + capture.IsIdentitySelector, + $"Expected a projected selector but got identity selector: {capture.Selector}"); + } + [Fact] public async Task AsSortDefinition_QueryContext_Custom_Field_Without_Member_Does_Not_Fail() { @@ -1427,6 +1459,46 @@ public string Name } } + public sealed class QueryContextRecordSelectorCapture + { + public bool IsIdentitySelector { get; set; } + + public string? Selector { get; set; } + } + + public class QueryContextRecordSelectorQuery + { + public QueryContextRecordSelectorEntity GetRecordSelector( + QueryContext query, + [Service] QueryContextRecordSelectorCapture capture) + { + var entity = new QueryContextRecordSelectorEntity + { + Id = Guid.NewGuid(), + FirstName = "A", + LastName = "B", + BirthDate = new DateOnly(2000, 1, 1) + }; + + capture.Selector = query.Selector?.ToString(); + capture.IsIdentitySelector = query.Selector is not null + && ReferenceEquals(query.Selector.Compile()(entity), entity); + + return entity; + } + } + + public sealed record QueryContextRecordSelectorEntity + { + public Guid Id { get; init; } + + public string FirstName { get; init; } = string.Empty; + + public string LastName { get; init; } = string.Empty; + + public DateOnly BirthDate { get; init; } + } + public class QueryContextCustomSortQuery { [UseSorting(typeof(CustomSortBookSortType))]