diff --git a/src/HotChocolate/ApolloFederation/src/ApolloFederation/Helpers/EntitiesResolver.cs b/src/HotChocolate/ApolloFederation/src/ApolloFederation/Helpers/EntitiesResolver.cs index 8adc3a8bae8..5ae07ae3198 100644 --- a/src/HotChocolate/ApolloFederation/src/ApolloFederation/Helpers/EntitiesResolver.cs +++ b/src/HotChocolate/ApolloFederation/src/ApolloFederation/Helpers/EntitiesResolver.cs @@ -1,6 +1,6 @@ +using System.Buffers; using System.Collections.Generic; using System.Threading.Tasks; -using HotChocolate.Language; using HotChocolate.Resolvers; using static HotChocolate.ApolloFederation.Constants.WellKnownContextData; @@ -11,39 +11,79 @@ namespace HotChocolate.ApolloFederation.Helpers; /// internal static class EntitiesResolver { - public static async Task> ResolveAsync( + public static async Task> ResolveAsync( ISchema schema, IReadOnlyList representations, IResolverContext context) { - var entities = new List(); + Task[] tasks = ArrayPool>.Shared.Rent(representations.Count); + var result = new object?[representations.Count]; - foreach (Representation representation in representations) + try { - if (schema.TryGetType(representation.TypeName, out var objectType) && - objectType.ContextData.TryGetValue(EntityResolver, out var value) && - value is FieldResolverDelegate resolver) + for (var i = 0; i < representations.Count; i++) { - context.SetLocalValue(TypeField, objectType); - context.SetLocalValue(DataField, representation.Data); + context.RequestAborted.ThrowIfCancellationRequested(); - var entity = await resolver.Invoke(context).ConfigureAwait(false); + Representation current = representations[i]; - if (entity is not null && - objectType!.ContextData.TryGetValue(ExternalSetter, out value) && - value is Action setExternals) + if (schema.TryGetType(current.TypeName, out ObjectType? objectType) && + objectType.ContextData.TryGetValue(EntityResolver, out var value) && + value is FieldResolverDelegate resolver) { - setExternals(objectType, representation.Data!, entity); - } + context.SetLocalState(TypeField, objectType); + context.SetLocalState(DataField, current.Data); - entities.Add(entity); + tasks[i] = resolver.Invoke(context).AsTask(); + } + else + { + throw ThrowHelper.EntityResolver_NoResolverFound(); + } } - else + + for (var i = 0; i < representations.Count; i++) { - throw ThrowHelper.EntityResolver_NoResolverFound(); + context.RequestAborted.ThrowIfCancellationRequested(); + + Task task = tasks[i]; + if (task.IsCompleted) + { + if (task.Exception is null) + { + result[i] = task.Result; + } + else + { + result[i] = null; + ReportError(context, i, task.Exception); + } + } + else + { + try + { + result[i] = await task; + } + catch (Exception ex) + { + result[i] = null; + ReportError(context, i, ex); + } + } } } + finally + { + ArrayPool>.Shared.Return(tasks, true); + } - return entities; + return result; + } + + private static void ReportError(IResolverContext context, int item, Exception ex) + { + Path itemPath = context.Path.Append(item); + context.ReportError(ex, error => error.SetPath(itemPath)); } } diff --git a/src/HotChocolate/ApolloFederation/test/ApolloFederation.Tests/EntitiesResolverTests.cs b/src/HotChocolate/ApolloFederation/test/ApolloFederation.Tests/EntitiesResolverTests.cs index ec1a61f4326..7247f9ca142 100644 --- a/src/HotChocolate/ApolloFederation/test/ApolloFederation.Tests/EntitiesResolverTests.cs +++ b/src/HotChocolate/ApolloFederation/test/ApolloFederation.Tests/EntitiesResolverTests.cs @@ -1,8 +1,16 @@ +using System; using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading; using System.Threading.Tasks; +using GreenDonut; using HotChocolate.ApolloFederation.Helpers; +using HotChocolate.Fetching; using HotChocolate.Language; using HotChocolate.Resolvers; +using Microsoft.Extensions.DependencyInjection; +using Moq; using Xunit; using static HotChocolate.ApolloFederation.TestHelper; @@ -24,13 +32,15 @@ public async void TestResolveViaForeignServiceType() // act var representations = new List { - new("ForeignType", new ObjectValueNode( - new ObjectFieldNode("id", "1"), - new ObjectFieldNode("someExternalField", "someExternalField"))) + new("ForeignType", + new ObjectValueNode( + new ObjectFieldNode("id", "1"), + new ObjectFieldNode("someExternalField", "someExternalField"))) }; // assert - List result = await EntitiesResolver.ResolveAsync(schema, representations, context); + IReadOnlyList result = + await EntitiesResolver.ResolveAsync(schema, representations, context); ForeignType obj = Assert.IsType(result[0]); Assert.Equal("1", obj.Id); Assert.Equal("someExternalField", obj.SomeExternalField); @@ -51,13 +61,15 @@ public async void TestResolveViaForeignServiceType_MixedTypes() // act var representations = new List { - new("MixedFieldTypes",new ObjectValueNode( - new ObjectFieldNode("id", "1"), - new ObjectFieldNode("intField", 25))) + new("MixedFieldTypes", + new ObjectValueNode( + new ObjectFieldNode("id", "1"), + new ObjectFieldNode("intField", 25))) }; // assert - List result = await EntitiesResolver.ResolveAsync(schema, representations, context); + IReadOnlyList result = + await EntitiesResolver.ResolveAsync(schema, representations, context); MixedFieldTypes obj = Assert.IsType(result[0]); Assert.Equal("1", obj.Id); Assert.Equal(25, obj.IntField); @@ -77,16 +89,54 @@ public async void TestResolveViaEntityResolver() // act var representations = new List { - new("TypeWithReferenceResolver", new ObjectValueNode(new ObjectFieldNode("Id", "1"))) + new("TypeWithReferenceResolver", + new ObjectValueNode(new ObjectFieldNode("Id", "1"))) }; // assert - List result = await EntitiesResolver.ResolveAsync(schema, representations, context); + IReadOnlyList result = + await EntitiesResolver.ResolveAsync(schema, representations, context); TypeWithReferenceResolver obj = Assert.IsType(result[0]); Assert.Equal("1", obj.Id); Assert.Equal("SomeField", obj.SomeField); } + [Fact] + public async void TestResolveViaEntityResolver_WithDataLoader() + { + // arrange + ISchema schema = SchemaBuilder.New() + .AddApolloFederation() + .AddQueryType() + .Create(); + + var batchScheduler = new ManualBatchScheduler(); + var dataLoader = new FederatedTypeDataLoader(batchScheduler); + + IResolverContext context = CreateResolverContext(schema, + null, + mock => + { + mock.Setup(c => c.Service()).Returns(dataLoader); + }); + + var representations = new List + { + new("FederatedType", new ObjectValueNode(new ObjectFieldNode("Id", "1"))), + new("FederatedType", new ObjectValueNode(new ObjectFieldNode("Id", "2"))), + new("FederatedType", new ObjectValueNode(new ObjectFieldNode("Id", "3"))) + }; + + // act + var resultTask = EntitiesResolver.ResolveAsync(schema, representations, context); + batchScheduler.Dispatch(); + var results = await resultTask; + + // assert + Assert.Equal(1, dataLoader.TimesCalled); + Assert.Equal(3, results.Count); + } + [Fact] public async void TestResolveViaEntityResolver_NoTypeFound() { @@ -135,6 +185,7 @@ public class Query public TypeWithReferenceResolver TypeWithReferenceResolver { get; set; } = default!; public TypeWithoutRefResolver TypeWithoutRefResolver { get; set; } = default!; public MixedFieldTypes MixedFieldTypes { get; set; } = default!; + public FederatedType TypeWithReferenceResolverMany { get; set; } = default!; } public class TypeWithoutRefResolver @@ -150,11 +201,7 @@ public class TypeWithReferenceResolver public static TypeWithReferenceResolver Get([LocalState] ObjectValueNode data) { - return new TypeWithReferenceResolver - { - Id = "1", - SomeField = "SomeField" - }; + return new TypeWithReferenceResolver {Id = "1", SomeField = "SomeField"}; } } @@ -202,4 +249,53 @@ public MixedFieldTypes(string id, int intField) [ReferenceResolver] public static MixedFieldTypes GetByExternal(string id, int intField) => new(id, intField); } + + [ExtendServiceType] + public class FederatedType + { + [Key] + [External] + public string Id { get; set; } = default!; + + public string SomeField { get; set; } = default!; + + [ReferenceResolver] + public static async Task GetById( + [LocalState] ObjectValueNode data, + [Service] FederatedTypeDataLoader loader) + { + var id = + data.Fields.FirstOrDefault(_ => _.Name.Value == "Id")?.Value.Value?.ToString() ?? + string.Empty; + + return await loader.LoadAsync(id); + } + } + + public class FederatedTypeDataLoader : BatchDataLoader + { + public int TimesCalled { get; private set; } + + public FederatedTypeDataLoader( + IBatchScheduler batchScheduler, + DataLoaderOptions? options = null) : base(batchScheduler, options) + { + } + + protected override Task> LoadBatchAsync( + IReadOnlyList keys, + CancellationToken cancellationToken) + { + TimesCalled++; + + Dictionary result = new() + { + ["1"] = new FederatedType {Id = "1", SomeField = "SomeField-1"}, + ["2"] = new FederatedType {Id = "2", SomeField = "SomeField-2"}, + ["3"] = new FederatedType {Id = "3", SomeField = "SomeField-3"} + }; + + return Task.FromResult>(result); + } + } } diff --git a/src/HotChocolate/ApolloFederation/test/ApolloFederation.Tests/HotChocolate.ApolloFederation.Tests.csproj b/src/HotChocolate/ApolloFederation/test/ApolloFederation.Tests/HotChocolate.ApolloFederation.Tests.csproj index 71a2ac486aa..63c3859fea2 100644 --- a/src/HotChocolate/ApolloFederation/test/ApolloFederation.Tests/HotChocolate.ApolloFederation.Tests.csproj +++ b/src/HotChocolate/ApolloFederation/test/ApolloFederation.Tests/HotChocolate.ApolloFederation.Tests.csproj @@ -6,6 +6,7 @@ + diff --git a/src/HotChocolate/ApolloFederation/test/ApolloFederation.Tests/TestHelper.cs b/src/HotChocolate/ApolloFederation/test/ApolloFederation.Tests/TestHelper.cs index 4ec631b8326..8573c493205 100644 --- a/src/HotChocolate/ApolloFederation/test/ApolloFederation.Tests/TestHelper.cs +++ b/src/HotChocolate/ApolloFederation/test/ApolloFederation.Tests/TestHelper.cs @@ -1,5 +1,7 @@ +using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Threading; using HotChocolate.Resolvers; using HotChocolate.Types; using Moq; @@ -8,11 +10,15 @@ namespace HotChocolate.ApolloFederation; public static class TestHelper { - public static IResolverContext CreateResolverContext(ISchema schema, ObjectType? type = null) + public static IResolverContext CreateResolverContext( + ISchema schema, + ObjectType? type = null, + Action>? additionalMockSetup = null) { var contextData = new Dictionary(); var mock = new Mock(MockBehavior.Strict); + mock.SetupGet(c => c.RequestAborted).Returns(CancellationToken.None); mock.SetupGet(c => c.ContextData).Returns(contextData); mock.SetupProperty(c => c.ScopedContextData); mock.SetupProperty(c => c.LocalContextData); @@ -23,6 +29,11 @@ public static IResolverContext CreateResolverContext(ISchema schema, ObjectType? mock.SetupGet(c => c.ObjectType).Returns(type); } + if (additionalMockSetup is not null) + { + additionalMockSetup(mock); + } + IResolverContext context = mock.Object; context.ScopedContextData = ImmutableDictionary.Empty; context.LocalContextData = ImmutableDictionary.Empty;