diff --git a/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionVisitor.cs b/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionVisitor.cs index a2a9adce4e8..2e9fdec99b0 100644 --- a/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionVisitor.cs +++ b/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionVisitor.cs @@ -1,7 +1,10 @@ using System.Linq.Expressions; using HotChocolate.Data.Projections.Expressions.Handlers; using HotChocolate.Execution.Processing; +using HotChocolate.Execution.Requirements; +using HotChocolate.Features; using HotChocolate.Types; +using HotChocolate.Types.Descriptors.Configurations; namespace HotChocolate.Data.Projections.Expressions; @@ -15,12 +18,19 @@ protected override ISelectionVisitorAction VisitObjectType( { var isAbstractType = field.Type.NamedType().IsAbstractType(); - if (!isAbstractType || !context.TryGetQueryableScope(out var scope)) + if (!context.TryGetQueryableScope(out var scope)) { return base.VisitObjectType(field, objectType, selection, context); } + // Collect requirements for all types (abstract and non-abstract) var selections = context.ResolverContext.GetSelections(objectType, selection, true); + CollectRequirements(selections, context, scope); + + if (!isAbstractType) + { + return base.VisitObjectType(field, objectType, selection, context); + } if (selections.Count == 0) { @@ -38,5 +48,62 @@ protected override ISelectionVisitorAction VisitObjectType( return res; } + private static void CollectRequirements( + IReadOnlyList selections, + QueryableProjectionContext context, + QueryableProjectionScope scope) + { + if (!context.ResolverContext.Schema.Features.TryGet(out var requirements)) + { + return; + } + + foreach (var selection in selections) + { + var flags = selection.Field.Flags; + if ((flags & CoreFieldFlags.WithRequirements) == CoreFieldFlags.WithRequirements) + { + var typeNode = requirements.GetRequirements(selection.Field); + if (typeNode is not null) + { + CollectRequiredProperties(typeNode, context, scope); + } + } + } + } + + private static void CollectRequiredProperties( + TypeNode typeNode, + QueryableProjectionContext context, + QueryableProjectionScope scope) + { + foreach (var propertyNode in typeNode.Nodes) + { + CollectRequiredProperty(propertyNode, context, scope); + } + } + + private static void CollectRequiredProperty( + PropertyNode propertyNode, + QueryableProjectionContext context, + QueryableProjectionScope scope) + { + var property = propertyNode.Property; + var instance = context.GetInstance(); + + // Check if already projected to avoid duplicates + foreach (var assignment in scope.Level.Peek()) + { + if (assignment.Member == property) + { + return; + } + } + + var propertyAccess = Expression.Property(instance, property); + var binding = Expression.Bind(property, propertyAccess); + scope.Level.Peek().Enqueue(binding); + } + public static readonly QueryableProjectionVisitor Default = new(); } diff --git a/src/HotChocolate/Data/test/Data.EntityFramework.Tests/IntegrationTests.cs b/src/HotChocolate/Data/test/Data.EntityFramework.Tests/IntegrationTests.cs index 5ec47bd8e07..62b45c01c21 100644 --- a/src/HotChocolate/Data/test/Data.EntityFramework.Tests/IntegrationTests.cs +++ b/src/HotChocolate/Data/test/Data.EntityFramework.Tests/IntegrationTests.cs @@ -459,4 +459,98 @@ public async Task ExecuteAsync_Should_ReturnNull_When_FirstOrDefaultZero_AsyncEn // assert result.MatchSnapshot(); } + + [Fact] + public async Task ExecuteAsync_Should_IncludeRequiredParentProperty_When_UsingParentRequiresWithString() + { + // arrange + var executor = await new ServiceCollection() + .AddGraphQL() + .AddType() + .AddFiltering() + .AddSorting() + .AddProjections() + .AddQueryType( + x => x + .Name("Query") + .Field("authors") + .Type() + .Resolve(Executable.From(_authors)) + .UseFirstOrDefault() + .UseProjection() + .UseFiltering() + .UseSorting()) + .BuildRequestExecutorAsync(); + + // act + var result = await executor.ExecuteAsync( + """ + { + authors { + requirement + } + } + """); + + // assert + result.MatchSnapshot(); + } + + [Fact] + public async Task ExecuteAsync_Should_IncludeRequiredParentProperty_When_UsingParentRequiresWithExpression() + { + // arrange + var executor = await new ServiceCollection() + .AddGraphQL() + .AddType() + .AddFiltering() + .AddSorting() + .AddProjections() + .AddQueryType( + x => x + .Name("Query") + .Field("authors") + .Type() + .Resolve(Executable.From(_authors)) + .UseFirstOrDefault() + .UseProjection() + .UseFiltering() + .UseSorting()) + .BuildRequestExecutorAsync(); + + // act + var result = await executor.ExecuteAsync( + """ + { + authors { + requirement + } + } + """); + + // assert + result.MatchSnapshot(); + } + + public class AuthorTypeWithStringRequirement : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor + .Field("requirement") + .ParentRequires(nameof(Author.Name)) + .Resolve(ctx => "Author Name: " + ctx.Parent().Name); + } + } + + public class AuthorTypeWithExpressionRequirement : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor + .Field("requirement") + .ParentRequires(t => new { t.Name }) + .Resolve(ctx => "Author Name: " + ctx.Parent().Name); + } + } } diff --git a/src/HotChocolate/Data/test/Data.EntityFramework.Tests/__snapshots__/IntegrationTests.ExecuteAsync_Should_IncludeRequiredParentProperty_When_UsingParentRequiresWithExpression.snap b/src/HotChocolate/Data/test/Data.EntityFramework.Tests/__snapshots__/IntegrationTests.ExecuteAsync_Should_IncludeRequiredParentProperty_When_UsingParentRequiresWithExpression.snap new file mode 100644 index 00000000000..13855e1c0c2 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.EntityFramework.Tests/__snapshots__/IntegrationTests.ExecuteAsync_Should_IncludeRequiredParentProperty_When_UsingParentRequiresWithExpression.snap @@ -0,0 +1,7 @@ +{ + "data": { + "authors": { + "requirement": "Author Name: Foo" + } + } +} diff --git a/src/HotChocolate/Data/test/Data.EntityFramework.Tests/__snapshots__/IntegrationTests.ExecuteAsync_Should_IncludeRequiredParentProperty_When_UsingParentRequiresWithString.snap b/src/HotChocolate/Data/test/Data.EntityFramework.Tests/__snapshots__/IntegrationTests.ExecuteAsync_Should_IncludeRequiredParentProperty_When_UsingParentRequiresWithString.snap new file mode 100644 index 00000000000..13855e1c0c2 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.EntityFramework.Tests/__snapshots__/IntegrationTests.ExecuteAsync_Should_IncludeRequiredParentProperty_When_UsingParentRequiresWithString.snap @@ -0,0 +1,7 @@ +{ + "data": { + "authors": { + "requirement": "Author Name: Foo" + } + } +}