Skip to content

Commit 51c1f8e

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

13 files changed

+1166
-140
lines changed

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

Lines changed: 208 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ internal sealed class FederationTypeInterceptor : TypeInterceptor
3636
BindingFlags.Static | BindingFlags.Public)!;
3737

3838
private readonly List<ObjectType> _entityTypes = [];
39+
private readonly List<InterfaceType> _entityInterfaceTypes = [];
3940
private readonly Dictionary<Uri, HashSet<string>> _imports = [];
4041
private IDescriptorContext _context = null!;
4142
private ITypeInspector _typeInspector = null!;
@@ -79,6 +80,25 @@ public override void OnAfterInitialize(
7980
objectType,
8081
objectTypeCfg,
8182
discoveryContext);
83+
return;
84+
}
85+
86+
if (discoveryContext.Type is InterfaceType interfaceType &&
87+
configuration is InterfaceTypeConfiguration interfaceTypeCfg)
88+
{
89+
ApplyMethodLevelReferenceResolvers(
90+
interfaceType,
91+
interfaceTypeCfg);
92+
93+
AddToUnionIfHasTypeLevelKeyDirective(
94+
interfaceType,
95+
interfaceTypeCfg);
96+
97+
AggregatePropertyLevelKeyDirectives(
98+
interfaceType,
99+
interfaceTypeCfg,
100+
discoveryContext);
101+
return;
82102
}
83103
}
84104

@@ -345,12 +365,24 @@ public override void OnAfterMakeExecutable(
345365
{
346366
CompleteExternalFieldSetters(type, typeCfg);
347367
CompleteReferenceResolver(typeCfg);
368+
return;
369+
}
370+
371+
if (completionContext.Type is InterfaceType interfaceType &&
372+
configuration is InterfaceTypeConfiguration interfaceTypeCfg)
373+
{
374+
CompleteExternalFieldSetters(interfaceType, interfaceTypeCfg);
375+
CompleteReferenceResolver(interfaceTypeCfg);
376+
return;
348377
}
349378
}
350379

351380
private void CompleteExternalFieldSetters(ObjectType type, ObjectTypeConfiguration typeCfg)
352381
=> ExternalSetterExpressionHelper.TryAddExternalSetter(type, typeCfg);
353382

383+
private void CompleteExternalFieldSetters(InterfaceType type, InterfaceTypeConfiguration typeCfg)
384+
=> ExternalSetterExpressionHelper.TryAddExternalSetter(type, typeCfg);
385+
354386
private void CompleteReferenceResolver(ObjectTypeConfiguration typeCfg)
355387
{
356388
var resolvers = typeCfg.Features.Get<List<ReferenceResolverConfiguration>>();
@@ -398,6 +430,53 @@ private void CompleteReferenceResolver(ObjectTypeConfiguration typeCfg)
398430
}
399431
}
400432

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

410489
var objectTypeCfg = (ObjectTypeConfiguration)definition!;
411490
objectTypeCfg.Fields.Add(ServerFields.CreateServiceField(_context));
412-
if (_entityTypes.Count > 0)
491+
if (_entityTypes.Count > 0 || _entityInterfaceTypes.Count > 0)
413492
{
414493
objectTypeCfg.Fields.Add(ServerFields.CreateEntitiesField(_context));
415494
}
@@ -452,6 +531,43 @@ private void ApplyMethodLevelReferenceResolvers(
452531
descriptor.CreateConfiguration();
453532
}
454533

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

588+
private void AddToUnionIfHasTypeLevelKeyDirective(
589+
InterfaceType interfaceType,
590+
InterfaceTypeConfiguration interfaceTypeCfg)
591+
{
592+
if (interfaceTypeCfg.Directives.FirstOrDefault(d => d.Value is KeyDirective) is { } keyDirective &&
593+
((KeyDirective)keyDirective.Value).Resolvable)
594+
{
595+
_entityInterfaceTypes.Add(interfaceType);
596+
return;
597+
}
598+
599+
if (interfaceTypeCfg.Fields.Any(f => f.Features.TryGet(out KeyMarker? key) && key.Resolvable))
600+
{
601+
_entityInterfaceTypes.Add(interfaceType);
602+
}
603+
}
604+
472605
private void AggregatePropertyLevelKeyDirectives(
473606
ObjectType objectType,
474607
ObjectTypeConfiguration objectTypeCfg,
@@ -532,6 +665,69 @@ private void AggregatePropertyLevelKeyDirectives(
532665
}
533666
}
534667

668+
private void AggregatePropertyLevelKeyDirectives(
669+
InterfaceType interfaceType,
670+
InterfaceTypeConfiguration interfaceTypeCfg,
671+
ITypeDiscoveryContext discoveryContext)
672+
{
673+
// if we find key markers on our fields, we need to construct the key directive
674+
// from the annotated fields.
675+
var foundMarkers = interfaceTypeCfg.Fields.Any(f => f.Features.TryGet(out KeyMarker? _));
676+
677+
if (!foundMarkers)
678+
{
679+
return;
680+
}
681+
682+
IReadOnlyList<InterfaceFieldConfiguration> fields = interfaceTypeCfg.Fields;
683+
var fieldSet = new StringBuilder();
684+
bool? resolvable = null;
685+
686+
foreach (var fieldDefinition in fields)
687+
{
688+
if (fieldDefinition.Features.TryGet(out KeyMarker? key))
689+
{
690+
if (resolvable is null)
691+
{
692+
resolvable = key.Resolvable;
693+
}
694+
else if (resolvable != key.Resolvable)
695+
{
696+
throw Key_FieldSet_ResolvableMustBeConsistent(fieldDefinition.Member!);
697+
}
698+
699+
if (fieldSet.Length > 0)
700+
{
701+
fieldSet.Append(' ');
702+
}
703+
704+
fieldSet.Append(fieldDefinition.Name);
705+
}
706+
}
707+
708+
// add the key directive with the dynamically generated field set.
709+
AddKeyDirective(interfaceTypeCfg, fieldSet.ToString(), resolvable ?? true);
710+
711+
// register dependency to the key directive so that it is completed before
712+
// we complete this type.
713+
foreach (var directiveDefinition in interfaceTypeCfg.Directives)
714+
{
715+
discoveryContext.Dependencies.Add(
716+
new TypeDependency(
717+
directiveDefinition.Type,
718+
TypeDependencyFulfilled.Completed));
719+
720+
discoveryContext.Dependencies.Add(new(directiveDefinition.Type));
721+
}
722+
723+
if (resolvable ?? true)
724+
{
725+
// since this type has now a key directive we also need to add this type to
726+
// the _Entity union type provided that the key is resolvable.
727+
_entityInterfaceTypes.Add(interfaceType);
728+
}
729+
}
730+
535731
private void AddMemberTypesToTheEntityUnionType(
536732
ITypeCompletionContext completionContext,
537733
TypeSystemConfiguration? definition)
@@ -556,4 +752,15 @@ private void AddKeyDirective(
556752
new KeyDirective(fieldSet, resolvable),
557753
_keyDirectiveReference));
558754
}
755+
756+
private void AddKeyDirective(
757+
InterfaceTypeConfiguration interfaceTypeCfg,
758+
string fieldSet,
759+
bool resolvable)
760+
{
761+
interfaceTypeCfg.Directives.Add(
762+
new DirectiveConfiguration(
763+
new KeyDirective(fieldSet, resolvable),
764+
_keyDirectiveReference));
765+
}
559766
}

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)