Skip to content

Commit e68cf64

Browse files
Added support for reference resolvers on an entity interface.
1 parent 03ba732 commit e68cf64

13 files changed

+1136
-139
lines changed

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

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,21 @@ 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+
interfaceType,
94+
interfaceTypeCfg,
95+
discoveryContext);
96+
return;
8297
}
8398
}
8499

@@ -345,12 +360,24 @@ public override void OnAfterMakeExecutable(
345360
{
346361
CompleteExternalFieldSetters(type, typeCfg);
347362
CompleteReferenceResolver(typeCfg);
363+
return;
364+
}
365+
366+
if (completionContext.Type is InterfaceType interfaceType &&
367+
configuration is InterfaceTypeConfiguration interfaceTypeCfg)
368+
{
369+
CompleteExternalFieldSetters(interfaceType, interfaceTypeCfg);
370+
CompleteReferenceResolver(interfaceTypeCfg);
371+
return;
348372
}
349373
}
350374

351375
private void CompleteExternalFieldSetters(ObjectType type, ObjectTypeConfiguration typeCfg)
352376
=> ExternalSetterExpressionHelper.TryAddExternalSetter(type, typeCfg);
353377

378+
private void CompleteExternalFieldSetters(InterfaceType type, InterfaceTypeConfiguration typeCfg)
379+
=> ExternalSetterExpressionHelper.TryAddExternalSetter(type, typeCfg);
380+
354381
private void CompleteReferenceResolver(ObjectTypeConfiguration typeCfg)
355382
{
356383
var resolvers = typeCfg.Features.Get<List<ReferenceResolverConfiguration>>();
@@ -398,6 +425,53 @@ private void CompleteReferenceResolver(ObjectTypeConfiguration typeCfg)
398425
}
399426
}
400427

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

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

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

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)