Skip to content

Commit

Permalink
Issue 4957 Correct problem with Apollo Federation entity resolver byp…
Browse files Browse the repository at this point in the history
…assing dataloader (#4958)
  • Loading branch information
mightymiracleman committed May 18, 2022
1 parent 72e410f commit ee288d0
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 35 deletions.
@@ -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;

Expand All @@ -11,39 +11,79 @@ namespace HotChocolate.ApolloFederation.Helpers;
/// </summary>
internal static class EntitiesResolver
{
public static async Task<List<object?>> ResolveAsync(
public static async Task<IReadOnlyList<object?>> ResolveAsync(
ISchema schema,
IReadOnlyList<Representation> representations,
IResolverContext context)
{
var entities = new List<object?>();
Task<object?>[] tasks = ArrayPool<Task<object?>>.Shared.Rent(representations.Count);
var result = new object?[representations.Count];

foreach (Representation representation in representations)
try
{
if (schema.TryGetType<ObjectType>(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.SetLocalState(TypeField, objectType);
context.SetLocalState(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<ObjectType, IValueNode, object> setExternals)
if (schema.TryGetType<ObjectType>(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<object?> 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<Task<object?>>.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));
}
}
@@ -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;

Expand All @@ -24,13 +32,15 @@ public async void TestResolveViaForeignServiceType()
// act
var representations = new List<Representation>
{
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<object?> result = await EntitiesResolver.ResolveAsync(schema, representations, context);
IReadOnlyList<object?> result =
await EntitiesResolver.ResolveAsync(schema, representations, context);
ForeignType obj = Assert.IsType<ForeignType>(result[0]);
Assert.Equal("1", obj.Id);
Assert.Equal("someExternalField", obj.SomeExternalField);
Expand All @@ -51,13 +61,15 @@ public async void TestResolveViaForeignServiceType_MixedTypes()
// act
var representations = new List<Representation>
{
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<object?> result = await EntitiesResolver.ResolveAsync(schema, representations, context);
IReadOnlyList<object?> result =
await EntitiesResolver.ResolveAsync(schema, representations, context);
MixedFieldTypes obj = Assert.IsType<MixedFieldTypes>(result[0]);
Assert.Equal("1", obj.Id);
Assert.Equal(25, obj.IntField);
Expand All @@ -77,16 +89,54 @@ public async void TestResolveViaEntityResolver()
// act
var representations = new List<Representation>
{
new("TypeWithReferenceResolver", new ObjectValueNode(new ObjectFieldNode("Id", "1")))
new("TypeWithReferenceResolver",
new ObjectValueNode(new ObjectFieldNode("Id", "1")))
};

// assert
List<object?> result = await EntitiesResolver.ResolveAsync(schema, representations, context);
IReadOnlyList<object?> result =
await EntitiesResolver.ResolveAsync(schema, representations, context);
TypeWithReferenceResolver obj = Assert.IsType<TypeWithReferenceResolver>(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<Query>()
.Create();

var batchScheduler = new ManualBatchScheduler();
var dataLoader = new FederatedTypeDataLoader(batchScheduler);

IResolverContext context = CreateResolverContext(schema,
null,
mock =>
{
mock.Setup(c => c.Service<FederatedTypeDataLoader>()).Returns(dataLoader);
});

var representations = new List<Representation>
{
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()
{
Expand Down Expand Up @@ -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
Expand All @@ -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"};
}
}

Expand Down Expand Up @@ -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<FederatedType> 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<string, FederatedType>
{
public int TimesCalled { get; private set; }

public FederatedTypeDataLoader(
IBatchScheduler batchScheduler,
DataLoaderOptions? options = null) : base(batchScheduler, options)
{
}

protected override Task<IReadOnlyDictionary<string, FederatedType>> LoadBatchAsync(
IReadOnlyList<string> keys,
CancellationToken cancellationToken)
{
TimesCalled++;

Dictionary<string, FederatedType> 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<IReadOnlyDictionary<string, FederatedType>>(result);
}
}
}
Expand Up @@ -6,6 +6,7 @@
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\..\GreenDonut\test\Core.Tests\GreenDonut.Tests.csproj" />
<ProjectReference Include="..\..\src\ApolloFederation\HotChocolate.ApolloFederation.csproj" />
</ItemGroup>

Expand Down
@@ -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;
Expand All @@ -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<Mock<IResolverContext>>? additionalMockSetup = null)
{
var contextData = new Dictionary<string, object?>();

var mock = new Mock<IResolverContext>(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);
Expand All @@ -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<string, object?>.Empty;
context.LocalContextData = ImmutableDictionary<string, object?>.Empty;
Expand Down

0 comments on commit ee288d0

Please sign in to comment.