Skip to content

Commit 75d5d0d

Browse files
Added support for reference resolvers on an entity interface.
1 parent 647810f commit 75d5d0d

13 files changed

+1132
-139
lines changed

src/HotChocolate/ApolloFederation/src/ApolloFederation/FederationTypeInterceptor.cs

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,19 @@ public override void OnAfterInitialize(
7979
objectType,
8080
objectTypeCfg,
8181
discoveryContext);
82+
return;
83+
}
84+
85+
if (discoveryContext.Type is InterfaceType interfaceType
86+
&& configuration is InterfaceTypeConfiguration interfaceTypeCfg)
87+
{
88+
ApplyMethodLevelReferenceResolvers(
89+
interfaceType,
90+
interfaceTypeCfg);
91+
92+
AggregatePropertyLevelKeyDirectives(
93+
interfaceTypeCfg,
94+
discoveryContext);
8295
}
8396
}
8497

@@ -345,12 +358,23 @@ public override void OnAfterMakeExecutable(
345358
{
346359
CompleteExternalFieldSetters(type, typeCfg);
347360
CompleteReferenceResolver(typeCfg);
361+
return;
362+
}
363+
364+
if (completionContext.Type is InterfaceType interfaceType
365+
&& configuration is InterfaceTypeConfiguration interfaceTypeCfg)
366+
{
367+
CompleteExternalFieldSetters(interfaceType, interfaceTypeCfg);
368+
CompleteReferenceResolver(interfaceTypeCfg);
348369
}
349370
}
350371

351372
private void CompleteExternalFieldSetters(ObjectType type, ObjectTypeConfiguration typeCfg)
352373
=> ExternalSetterExpressionHelper.TryAddExternalSetter(type, typeCfg);
353374

375+
private void CompleteExternalFieldSetters(InterfaceType type, InterfaceTypeConfiguration typeCfg)
376+
=> ExternalSetterExpressionHelper.TryAddExternalSetter(type, typeCfg);
377+
354378
private void CompleteReferenceResolver(ObjectTypeConfiguration typeCfg)
355379
{
356380
var resolvers = typeCfg.Features.Get<List<ReferenceResolverConfiguration>>();
@@ -398,6 +422,53 @@ private void CompleteReferenceResolver(ObjectTypeConfiguration typeCfg)
398422
}
399423
}
400424

425+
private void CompleteReferenceResolver(InterfaceTypeConfiguration typeCfg)
426+
{
427+
var resolvers = typeCfg.Features.Get<List<ReferenceResolverConfiguration>>();
428+
429+
if (resolvers is null)
430+
{
431+
return;
432+
}
433+
434+
if (resolvers.Count == 1)
435+
{
436+
typeCfg.Features.Set(new ReferenceResolver(resolvers[0].Resolver));
437+
}
438+
else
439+
{
440+
var expressions = new Stack<(Expression Condition, Expression Execute)>();
441+
var context = Expression.Parameter(typeof(IResolverContext));
442+
443+
foreach (var resolverDef in resolvers)
444+
{
445+
Expression required = Expression.Constant(resolverDef.Required);
446+
Expression resolver = Expression.Constant(resolverDef.Resolver);
447+
Expression condition = Expression.Call(s_matches, context, required);
448+
Expression execute = Expression.Call(s_execute, context, resolver);
449+
expressions.Push((condition, execute));
450+
}
451+
452+
Expression current = Expression.Call(s_invalid, context);
453+
var variable = Expression.Variable(typeof(ValueTask<object?>));
454+
455+
while (expressions.Count > 0)
456+
{
457+
var expression = expressions.Pop();
458+
current = Expression.IfThenElse(
459+
expression.Condition,
460+
Expression.Assign(variable, expression.Execute),
461+
current);
462+
}
463+
464+
current = Expression.Block([variable], current, variable);
465+
466+
typeCfg.Features.Set(
467+
new ReferenceResolver(
468+
Expression.Lambda<FieldResolverDelegate>(current, context).Compile()));
469+
}
470+
}
471+
401472
private void AddServiceTypeToQueryType(
402473
ITypeCompletionContext completionContext,
403474
TypeSystemConfiguration? definition)
@@ -452,6 +523,43 @@ private void ApplyMethodLevelReferenceResolvers(
452523
descriptor.CreateConfiguration();
453524
}
454525

526+
private void ApplyMethodLevelReferenceResolvers(
527+
InterfaceType interfaceType,
528+
InterfaceTypeConfiguration interfaceTypeCfg)
529+
{
530+
if (interfaceType.RuntimeType == typeof(object))
531+
{
532+
return;
533+
}
534+
535+
var descriptor = InterfaceTypeDescriptor.From(_context, interfaceTypeCfg);
536+
537+
// Static methods won't end up in the schema as fields.
538+
// The default initialization system only considers instance methods,
539+
// so we have to handle the attributes for those manually.
540+
var potentiallyUnregisteredReferenceResolvers = interfaceType.RuntimeType
541+
.GetMethods(BindingFlags.Static | BindingFlags.Public);
542+
543+
foreach (var possibleReferenceResolver in potentiallyUnregisteredReferenceResolvers)
544+
{
545+
if (!possibleReferenceResolver.IsDefined(typeof(ReferenceResolverAttribute)))
546+
{
547+
continue;
548+
}
549+
550+
foreach (var attribute in possibleReferenceResolver.GetCustomAttributes(true))
551+
{
552+
if (attribute is ReferenceResolverAttribute casted)
553+
{
554+
casted.TryConfigure(_context, descriptor, possibleReferenceResolver);
555+
}
556+
}
557+
}
558+
559+
// This seems to re-detect the entity resolver and save it into the context data.
560+
descriptor.CreateConfiguration();
561+
}
562+
455563
private void AddToUnionIfHasTypeLevelKeyDirective(
456564
ObjectType objectType,
457565
ObjectTypeConfiguration objectTypeCfg)
@@ -532,6 +640,61 @@ private void AggregatePropertyLevelKeyDirectives(
532640
}
533641
}
534642

643+
private void AggregatePropertyLevelKeyDirectives(
644+
InterfaceTypeConfiguration interfaceTypeCfg,
645+
ITypeDiscoveryContext discoveryContext)
646+
{
647+
// if we find key markers on our fields, we need to construct the key directive
648+
// from the annotated fields.
649+
var foundMarkers = interfaceTypeCfg.Fields.Any(f => f.Features.TryGet(out KeyMarker? _));
650+
651+
if (!foundMarkers)
652+
{
653+
return;
654+
}
655+
656+
IReadOnlyList<InterfaceFieldConfiguration> fields = interfaceTypeCfg.Fields;
657+
var fieldSet = new StringBuilder();
658+
bool? resolvable = null;
659+
660+
foreach (var fieldDefinition in fields)
661+
{
662+
if (fieldDefinition.Features.TryGet(out KeyMarker? key))
663+
{
664+
if (resolvable is null)
665+
{
666+
resolvable = key.Resolvable;
667+
}
668+
else if (resolvable != key.Resolvable)
669+
{
670+
throw Key_FieldSet_ResolvableMustBeConsistent(fieldDefinition.Member!);
671+
}
672+
673+
if (fieldSet.Length > 0)
674+
{
675+
fieldSet.Append(' ');
676+
}
677+
678+
fieldSet.Append(fieldDefinition.Name);
679+
}
680+
}
681+
682+
// add the key directive with the dynamically generated field set.
683+
AddKeyDirective(interfaceTypeCfg, fieldSet.ToString(), resolvable ?? true);
684+
685+
// register dependency to the key directive so that it is completed before
686+
// we complete this type.
687+
foreach (var directiveDefinition in interfaceTypeCfg.Directives)
688+
{
689+
discoveryContext.Dependencies.Add(
690+
new TypeDependency(
691+
directiveDefinition.Type,
692+
TypeDependencyFulfilled.Completed));
693+
694+
discoveryContext.Dependencies.Add(new(directiveDefinition.Type));
695+
}
696+
}
697+
535698
private void AddMemberTypesToTheEntityUnionType(
536699
ITypeCompletionContext completionContext,
537700
TypeSystemConfiguration? definition)
@@ -556,4 +719,15 @@ private void AddKeyDirective(
556719
new KeyDirective(fieldSet, resolvable),
557720
_keyDirectiveReference));
558721
}
722+
723+
private void AddKeyDirective(
724+
InterfaceTypeConfiguration interfaceTypeCfg,
725+
string fieldSet,
726+
bool resolvable)
727+
{
728+
interfaceTypeCfg.Directives.Add(
729+
new DirectiveConfiguration(
730+
new KeyDirective(fieldSet, resolvable),
731+
_keyDirectiveReference));
732+
}
559733
}

src/HotChocolate/ApolloFederation/src/ApolloFederation/Resolvers/EntitiesResolver.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,24 @@ internal static class EntitiesResolver
3434
entityContext.SetLocalState(DataField, current.Data);
3535

3636
tasks[i] = entity.Resolver.Invoke(entityContext).AsTask();
37+
continue;
3738
}
38-
else
39+
40+
if (schema.Types.TryGetType<InterfaceType>(current.TypeName, out var interfaceType)
41+
&& interfaceType.Features.TryGet(out ReferenceResolver? entityInterface))
3942
{
40-
throw ThrowHelper.EntityResolver_NoResolverFound();
43+
// We clone the resolver context here so that we can split the work
44+
// into subtasks that can be awaited in parallel and produce separate results.
45+
var entityContext = context.Clone();
46+
47+
entityContext.SetLocalState(TypeField, interfaceType);
48+
entityContext.SetLocalState(DataField, current.Data);
49+
50+
tasks[i] = entityInterface.Resolver.Invoke(entityContext).AsTask();
51+
continue;
4152
}
53+
54+
throw ThrowHelper.EntityResolver_NoResolverFound();
4255
}
4356

4457
for (var i = 0; i < representations.Count; i++)

src/HotChocolate/ApolloFederation/src/ApolloFederation/Resolvers/ExternalSetterExpressionHelper.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,30 @@ public static void TryAddExternalSetter(ObjectType type, ObjectTypeConfiguration
4747
}
4848
}
4949

50+
public static void TryAddExternalSetter(InterfaceType type, InterfaceTypeConfiguration typeDef)
51+
{
52+
List<Expression>? block = null;
53+
54+
foreach (var field in type.Fields)
55+
{
56+
if (field.Directives.ContainsDirective<ExternalDirective>()
57+
&& typeDef.Fields.FirstOrDefault(f => f.Name == field.Name) is
58+
{ Member: PropertyInfo { SetMethod: not null } property })
59+
{
60+
var expression = CreateTrySetValue(type.RuntimeType, property, field.Name);
61+
(block ??= []).Add(expression);
62+
}
63+
}
64+
65+
if (block is not null)
66+
{
67+
typeDef.Features.Set(new ExternalSetter(
68+
Lambda<Action<ObjectType, IValueNode, object>>(
69+
Block(block), s_type, s_data, s_entity)
70+
.Compile()));
71+
}
72+
}
73+
5074
private static Expression CreateTrySetValue(
5175
Type runtimeType,
5276
PropertyInfo property,

src/HotChocolate/ApolloFederation/src/ApolloFederation/Resolvers/ReferenceResolverArgumentExpressionBuilder.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ internal sealed class ReferenceResolverArgumentExpressionBuilder :
1414
nameof(ArgumentParser.GetValue),
1515
BindingFlags.Static | BindingFlags.Public)!;
1616

17+
private readonly Type _targetType;
18+
19+
public ReferenceResolverArgumentExpressionBuilder(Type targetType)
20+
{
21+
ArgumentNullException.ThrowIfNull(targetType);
22+
_targetType = targetType;
23+
}
24+
1725
public override Expression Build(ParameterExpressionBuilderContext context)
1826
{
1927
var param = context.Parameter;
@@ -35,7 +43,7 @@ public override Expression Build(ParameterExpressionBuilderContext context)
3543
param,
3644
typeKey,
3745
context.ResolverContext,
38-
typeof(ObjectType));
46+
_targetType);
3947
var getValueMethod = _getValue.MakeGenericMethod(param.ParameterType);
4048
var getValue = Expression.Call(
4149
getValueMethod,

0 commit comments

Comments
 (0)