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
30 changes: 26 additions & 4 deletions src/HotChocolate/Core/src/Types/Types/Relay/NodeFieldResolvers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@ public static async ValueTask ResolveSingleNodeAsync(
if (context.Schema.Types.TryGetType<ObjectType>(typeName, out var type)
&& type.Features.Get<NodeTypeFeature>() is { NodeResolver: not null } feature)
{
var typeConverter = context.Service<ITypeConverter>();
SetLocalContext(context, nodeId, deserializedId, type);
TryReplaceArguments(context, feature.NodeResolver, Id, nodeId);
await feature.NodeResolver.Pipeline.Invoke(context);
context.Result = CoerceResult(context.Result, type, typeConverter);
}
else
{
Expand Down Expand Up @@ -74,6 +76,7 @@ public static async ValueTask ResolveManyNodeAsync(
var tasks = ArrayPool<Task<object?>>.Shared.Rent(list.Items.Count);
var results = new object?[list.Items.Count];
var ct = context.RequestAborted;
var typeConverter = context.Service<ITypeConverter>();

for (var i = 0; i < list.Items.Count; i++)
{
Expand All @@ -90,7 +93,7 @@ public static async ValueTask ResolveManyNodeAsync(
var nodeContext = context.Clone();
SetLocalContext(nodeContext, nodeId, deserializedId, type);
TryReplaceArguments(nodeContext, feature.NodeResolver, Ids, nodeId);
tasks[i] = ExecutePipelineAsync(nodeContext, feature.NodeResolver);
tasks[i] = ExecutePipelineAsync(nodeContext, type, feature.NodeResolver, typeConverter);
}
else
{
Expand Down Expand Up @@ -158,12 +161,13 @@ public static async ValueTask ResolveManyNodeAsync(
if (schema.Types.TryGetType<ObjectType>(typeName, out var type)
&& type.Features.Get<NodeTypeFeature>() is { NodeResolver: not null } feature)
{
var typeConverter = context.Service<ITypeConverter>();
var nodeContext = context.Clone();

SetLocalContext(nodeContext, nodeId, deserializedId, type);
TryReplaceArguments(nodeContext, feature.NodeResolver, Ids, nodeId);

var result = await ExecutePipelineAsync(nodeContext, feature.NodeResolver);
var result = await ExecutePipelineAsync(nodeContext, type, feature.NodeResolver, typeConverter);

if (result is IError error)
{
Expand Down Expand Up @@ -192,10 +196,12 @@ public static async ValueTask ResolveManyNodeAsync(

static async Task<object?> ExecutePipelineAsync(
IMiddlewareContext nodeResolverContext,
NodeResolverInfo nodeResolverInfo)
ObjectType type,
NodeResolverInfo nodeResolverInfo,
ITypeConverter typeConverter)
{
await nodeResolverInfo.Pipeline.Invoke(nodeResolverContext).ConfigureAwait(false);
return nodeResolverContext.Result;
return CoerceResult(nodeResolverContext.Result, type, typeConverter);
}
}

Expand Down Expand Up @@ -242,6 +248,22 @@ private static void TryReplaceArguments(
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? CoerceResult(
object? result,
ObjectType type,
ITypeConverter typeConverter)
{
if (result is null || result is IError || type.RuntimeType.IsInstanceOfType(result))
{
return result;
}

return typeConverter.TryConvert(type.RuntimeType, result, out var converted)
? converted
: result;
}

private static void ReportError(IResolverContext context, int item, Exception ex)
=> context.ReportError(ex, error => error.SetPath(context.Path.Append(item)));
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Text.Json;
using HotChocolate.Execution;
using HotChocolate.Tests;
using HotChocolate.Types.Relay;
Expand Down Expand Up @@ -111,6 +112,55 @@ public async Task Infer_Node_From_Query_Field_Resolve_Node()
.MatchSnapshotAsync();
}

[Fact]
public async Task Infer_Node_From_Query_Field_Resolve_Node_With_Runtime_Type_Conversion()
{
var executor = await new ServiceCollection()
.AddGraphQL()
.AddQueryType<Query10Type>()
.AddGlobalObjectIdentification()
.AddTypeConverter<FooWrapper, Foo>(wrapper => wrapper.Value)
.BuildRequestExecutorAsync();

var serializer = executor.Schema.Services.GetRequiredService<INodeIdSerializer>();
var id = serializer.Format("Foo", "abc");

var result = await executor.ExecuteAsync(
OperationRequestBuilder.New()
.SetDocument(
"""
query ($id: ID!) {
node(id: $id) {
... on Foo {
id
clearTextId
}
}
nodes(ids: [$id, $id]) {
... on Foo {
clearTextId
}
}
}
""")
.SetVariableValues(new Dictionary<string, object?> { { "id", id } })
.Build());

var operationResult = result.ExpectOperationResult();

Assert.True(
operationResult.Errors is null || operationResult.Errors.Count == 0,
$"Expected no errors but got: {operationResult.ToJson()}");

using var document = JsonDocument.Parse(operationResult.ToJson());
var data = document.RootElement.GetProperty("data");

Assert.Equal(id, data.GetProperty("node").GetProperty("id").GetString());
Assert.Equal("abc", data.GetProperty("node").GetProperty("clearTextId").GetString());
Assert.Equal("abc", data.GetProperty("nodes")[0].GetProperty("clearTextId").GetString());
Assert.Equal("abc", data.GetProperty("nodes")[1].GetProperty("clearTextId").GetString());
}

[Fact]
public async Task Infer_Node_From_Query_Field_With_Abc_Argument_Resolve_Node()
{
Expand Down Expand Up @@ -267,6 +317,18 @@ public class Query2
public Foo GetFooById(string abc) => new(abc);
}

public class Query10
{
[NodeResolver]
public object GetFooById(string id) => new FooWrapper(new Foo(id));
}

public class Query10Type : ObjectType<Query10>
{
protected override void Configure(IObjectTypeDescriptor<Query10> descriptor)
=> descriptor.Field(t => t.GetFooById(default!)).Type<ObjectType<Foo>>();
}

public class Query3
{
[NodeResolver]
Expand Down Expand Up @@ -299,6 +361,11 @@ public class Foo(string id)
public string ClearTextId => Id;
}

public sealed class FooWrapper(Foo value)
{
public Foo Value { get; } = value;
}

public class Bar(int id)
{
public int Id { get; } = id;
Expand Down
Loading