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 @@ -298,22 +298,28 @@ private void CollectSelection(
Selection selection,
TypeNode parent)
{
var namedType = selection.Field.Type.NamedType();
var field = selection.Field;
var namedType = field.Type.NamedType();

if (selection.Field.PureResolver is null
|| selection.Field.ResolverMember?.ReflectedType != selection.Field.DeclaringType.RuntimeType)
// A field is projectable if its resolver is the underlying member (a pure resolver
// declared on the parent runtime type) or if it explicitly replaces that member
// (fluent ResolveWith / [BindMember]).
var isPureMemberResolver = field.PureResolver is not null
&& field.ResolverMember?.ReflectedType == field.DeclaringType.RuntimeType;
var isMemberReplacement = field.Flags.HasFlag(CoreFieldFlags.MemberReplacement);

if (!isPureMemberResolver && !isMemberReplacement)
{
return;
}

if (selection.Field.Member is not PropertyInfo { CanRead: true, CanWrite: true } property)
if (field.Member is not PropertyInfo { CanRead: true, CanWrite: true } property)
{
return;
}

var flags = selection.Field.Flags;
if ((flags & CoreFieldFlags.Connection) == CoreFieldFlags.Connection
|| (flags & CoreFieldFlags.CollectionSegment) == CoreFieldFlags.CollectionSegment)
if (field.Flags.HasFlag(CoreFieldFlags.Connection)
|| field.Flags.HasFlag(CoreFieldFlags.CollectionSegment))
{
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,6 @@ internal enum CoreFieldFlags : long
WithRequirements = 1 << 30,
UsesProjections = 1L << 31,
ImplicitField = 1L << 32,
BatchResolver = 1L << 33
BatchResolver = 1L << 33,
MemberReplacement = 1L << 34
}
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,11 @@ private IObjectFieldDescriptor ResolveWithInternal(
Configuration.Resolver = null;
Configuration.ResultType = propertyOrMethod.GetReturnType();

if (Configuration.Member is not null)
{
Configuration.Flags |= CoreFieldFlags.MemberReplacement;
}

if (propertyOrMethod is MethodInfo m)
{
_parameterInfos = Context.TypeInspector.GetParameters(m);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using HotChocolate.Execution.Processing;
using HotChocolate.Types;
using HotChocolate.Types.Descriptors.Configurations;

namespace HotChocolate.Data.Projections.Expressions.Handlers;

Expand Down Expand Up @@ -32,7 +33,14 @@ protected static bool CanProjectMember(Selection selection)
return true;
}

// When a member is explicitly bound we keep projecting it.
// Explicit member replacements (e.g. fluent ResolveWith on a shadowed property)
// must keep projecting the underlying member so custom resolvers
// can access the shadowed data on projected parents.
if (selection.Field.Flags.HasFlag(CoreFieldFlags.MemberReplacement))
{
return true;
}

return resolverMember.IsDefined(typeof(BindMemberAttribute), inherit: true);
}

Expand Down
155 changes: 155 additions & 0 deletions src/HotChocolate/Data/test/Data.Tests/ResolveWithProjectionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
using HotChocolate.Execution;
using HotChocolate.Execution.Processing;
using HotChocolate.Types;
using Microsoft.Extensions.DependencyInjection;

namespace HotChocolate.Data;

public class ResolveWithProjectionTests
{
[Fact]
public async Task ResolveWith_Should_Project_Member_When_UseProjection_Is_Applied_On_Parent()
{
// arrange
var executor = await new ServiceCollection()
.AddGraphQL()
.AddProjections()
.AddQueryType<Query>()
.AddType<TenantType>()
.BuildRequestExecutorAsync();

// act
var result = await executor.ExecuteAsync(
"""
{
tenants {
workspaces {
id
}
}
}
""");

// assert
result.MatchInlineSnapshot(
"""
{
"data": {
"tenants": [
{
"workspaces": [
{
"id": 2
},
{
"id": 4
}
]
}
]
}
}
""");
}

[Fact]
public async Task ResolveWith_Should_Project_Member_When_AsSelector_Is_Used_On_Parent()
{
// arrange
var executor = await new ServiceCollection()
.AddGraphQL()
.AddQueryType<AsSelectorQuery>()
.AddType<TenantType>()
.BuildRequestExecutorAsync();

// act
var result = await executor.ExecuteAsync(
"""
{
tenants {
workspaces {
id
}
}
}
""");

// assert
result.MatchInlineSnapshot(
"""
{
"data": {
"tenants": [
{
"workspaces": [
{
"id": 2
},
{
"id": 4
}
]
}
]
}
}
""");
}

public class Query
{
[UseProjection]
public IQueryable<Tenant> GetTenants()
=> CreateTenants().AsQueryable();
}

public class AsSelectorQuery
{
public IQueryable<Tenant> GetTenants(ISelection selection)
=> CreateTenants().AsQueryable().Select(selection.AsSelector<Tenant>());
}

private static Tenant[] CreateTenants()
=>
[
new Tenant
{
Id = 1,
Workspaces =
[
new Workspace { Id = 1 },
new Workspace { Id = 2 },
new Workspace { Id = 3 },
new Workspace { Id = 4 }
]
}
];

public class TenantType : ObjectType<Tenant>
{
protected override void Configure(IObjectTypeDescriptor<Tenant> descriptor)
{
descriptor
.Field(t => t.Workspaces)
.ResolveWith<TenantResolvers>(r => r.GetWorkspaces(default!));
}
}

public class TenantResolvers
{
public IQueryable<Workspace> GetWorkspaces([Parent] Tenant tenant)
=> tenant.Workspaces.Where(w => w.Id % 2 == 0).AsQueryable();
}

public class Tenant
{
public int Id { get; set; }

public List<Workspace> Workspaces { get; set; } = [];
}

public class Workspace
{
public int Id { get; set; }
}
}
Loading