diff --git a/src/HotChocolate/ApolloFederation/src/ApolloFederation/FederationTypeInterceptor.cs b/src/HotChocolate/ApolloFederation/src/ApolloFederation/FederationTypeInterceptor.cs index f95d912fb68..eda5c836694 100644 --- a/src/HotChocolate/ApolloFederation/src/ApolloFederation/FederationTypeInterceptor.cs +++ b/src/HotChocolate/ApolloFederation/src/ApolloFederation/FederationTypeInterceptor.cs @@ -79,6 +79,19 @@ public override void OnAfterInitialize( objectType, objectTypeCfg, discoveryContext); + return; + } + + if (discoveryContext.Type is InterfaceType interfaceType + && configuration is InterfaceTypeConfiguration interfaceTypeCfg) + { + ApplyMethodLevelReferenceResolvers( + interfaceType, + interfaceTypeCfg); + + AggregatePropertyLevelKeyDirectives( + interfaceTypeCfg, + discoveryContext); } } @@ -345,12 +358,23 @@ public override void OnAfterMakeExecutable( { CompleteExternalFieldSetters(type, typeCfg); CompleteReferenceResolver(typeCfg); + return; + } + + if (completionContext.Type is InterfaceType interfaceType + && configuration is InterfaceTypeConfiguration interfaceTypeCfg) + { + CompleteExternalFieldSetters(interfaceType, interfaceTypeCfg); + CompleteReferenceResolver(interfaceTypeCfg); } } private void CompleteExternalFieldSetters(ObjectType type, ObjectTypeConfiguration typeCfg) => ExternalSetterExpressionHelper.TryAddExternalSetter(type, typeCfg); + private void CompleteExternalFieldSetters(InterfaceType type, InterfaceTypeConfiguration typeCfg) + => ExternalSetterExpressionHelper.TryAddExternalSetter(type, typeCfg); + private void CompleteReferenceResolver(ObjectTypeConfiguration typeCfg) { var resolvers = typeCfg.Features.Get>(); @@ -398,6 +422,53 @@ private void CompleteReferenceResolver(ObjectTypeConfiguration typeCfg) } } + private void CompleteReferenceResolver(InterfaceTypeConfiguration typeCfg) + { + var resolvers = typeCfg.Features.Get>(); + + if (resolvers is null) + { + return; + } + + if (resolvers.Count == 1) + { + typeCfg.Features.Set(new ReferenceResolver(resolvers[0].Resolver)); + } + else + { + var expressions = new Stack<(Expression Condition, Expression Execute)>(); + var context = Expression.Parameter(typeof(IResolverContext)); + + foreach (var resolverDef in resolvers) + { + Expression required = Expression.Constant(resolverDef.Required); + Expression resolver = Expression.Constant(resolverDef.Resolver); + Expression condition = Expression.Call(s_matches, context, required); + Expression execute = Expression.Call(s_execute, context, resolver); + expressions.Push((condition, execute)); + } + + Expression current = Expression.Call(s_invalid, context); + var variable = Expression.Variable(typeof(ValueTask)); + + while (expressions.Count > 0) + { + var expression = expressions.Pop(); + current = Expression.IfThenElse( + expression.Condition, + Expression.Assign(variable, expression.Execute), + current); + } + + current = Expression.Block([variable], current, variable); + + typeCfg.Features.Set( + new ReferenceResolver( + Expression.Lambda(current, context).Compile())); + } + } + private void AddServiceTypeToQueryType( ITypeCompletionContext completionContext, TypeSystemConfiguration? definition) @@ -452,6 +523,43 @@ private void ApplyMethodLevelReferenceResolvers( descriptor.CreateConfiguration(); } + private void ApplyMethodLevelReferenceResolvers( + InterfaceType interfaceType, + InterfaceTypeConfiguration interfaceTypeCfg) + { + if (interfaceType.RuntimeType == typeof(object)) + { + return; + } + + var descriptor = InterfaceTypeDescriptor.From(_context, interfaceTypeCfg); + + // Static methods won't end up in the schema as fields. + // The default initialization system only considers instance methods, + // so we have to handle the attributes for those manually. + var potentiallyUnregisteredReferenceResolvers = interfaceType.RuntimeType + .GetMethods(BindingFlags.Static | BindingFlags.Public); + + foreach (var possibleReferenceResolver in potentiallyUnregisteredReferenceResolvers) + { + if (!possibleReferenceResolver.IsDefined(typeof(ReferenceResolverAttribute))) + { + continue; + } + + foreach (var attribute in possibleReferenceResolver.GetCustomAttributes(true)) + { + if (attribute is ReferenceResolverAttribute casted) + { + casted.TryConfigure(_context, descriptor, possibleReferenceResolver); + } + } + } + + // This seems to re-detect the entity resolver and save it into the context data. + descriptor.CreateConfiguration(); + } + private void AddToUnionIfHasTypeLevelKeyDirective( ObjectType objectType, ObjectTypeConfiguration objectTypeCfg) @@ -532,6 +640,61 @@ private void AggregatePropertyLevelKeyDirectives( } } + private void AggregatePropertyLevelKeyDirectives( + InterfaceTypeConfiguration interfaceTypeCfg, + ITypeDiscoveryContext discoveryContext) + { + // if we find key markers on our fields, we need to construct the key directive + // from the annotated fields. + var foundMarkers = interfaceTypeCfg.Fields.Any(f => f.Features.TryGet(out KeyMarker? _)); + + if (!foundMarkers) + { + return; + } + + IReadOnlyList fields = interfaceTypeCfg.Fields; + var fieldSet = new StringBuilder(); + bool? resolvable = null; + + foreach (var fieldDefinition in fields) + { + if (fieldDefinition.Features.TryGet(out KeyMarker? key)) + { + if (resolvable is null) + { + resolvable = key.Resolvable; + } + else if (resolvable != key.Resolvable) + { + throw Key_FieldSet_ResolvableMustBeConsistent(fieldDefinition.Member!); + } + + if (fieldSet.Length > 0) + { + fieldSet.Append(' '); + } + + fieldSet.Append(fieldDefinition.Name); + } + } + + // add the key directive with the dynamically generated field set. + AddKeyDirective(interfaceTypeCfg, fieldSet.ToString(), resolvable ?? true); + + // register dependency to the key directive so that it is completed before + // we complete this type. + foreach (var directiveDefinition in interfaceTypeCfg.Directives) + { + discoveryContext.Dependencies.Add( + new TypeDependency( + directiveDefinition.Type, + TypeDependencyFulfilled.Completed)); + + discoveryContext.Dependencies.Add(new(directiveDefinition.Type)); + } + } + private void AddMemberTypesToTheEntityUnionType( ITypeCompletionContext completionContext, TypeSystemConfiguration? definition) @@ -556,4 +719,15 @@ private void AddKeyDirective( new KeyDirective(fieldSet, resolvable), _keyDirectiveReference)); } + + private void AddKeyDirective( + InterfaceTypeConfiguration interfaceTypeCfg, + string fieldSet, + bool resolvable) + { + interfaceTypeCfg.Directives.Add( + new DirectiveConfiguration( + new KeyDirective(fieldSet, resolvable), + _keyDirectiveReference)); + } } diff --git a/src/HotChocolate/ApolloFederation/src/ApolloFederation/Resolvers/EntitiesResolver.cs b/src/HotChocolate/ApolloFederation/src/ApolloFederation/Resolvers/EntitiesResolver.cs index 0f26ee75a19..14a05d28ebe 100644 --- a/src/HotChocolate/ApolloFederation/src/ApolloFederation/Resolvers/EntitiesResolver.cs +++ b/src/HotChocolate/ApolloFederation/src/ApolloFederation/Resolvers/EntitiesResolver.cs @@ -34,11 +34,24 @@ internal static class EntitiesResolver entityContext.SetLocalState(DataField, current.Data); tasks[i] = entity.Resolver.Invoke(entityContext).AsTask(); + continue; } - else + + if (schema.Types.TryGetType(current.TypeName, out var interfaceType) + && interfaceType.Features.TryGet(out ReferenceResolver? entityInterface)) { - throw ThrowHelper.EntityResolver_NoResolverFound(); + // We clone the resolver context here so that we can split the work + // into subtasks that can be awaited in parallel and produce separate results. + var entityContext = context.Clone(); + + entityContext.SetLocalState(TypeField, interfaceType); + entityContext.SetLocalState(DataField, current.Data); + + tasks[i] = entityInterface.Resolver.Invoke(entityContext).AsTask(); + continue; } + + throw ThrowHelper.EntityResolver_NoResolverFound(); } for (var i = 0; i < representations.Count; i++) diff --git a/src/HotChocolate/ApolloFederation/src/ApolloFederation/Resolvers/ExternalSetterExpressionHelper.cs b/src/HotChocolate/ApolloFederation/src/ApolloFederation/Resolvers/ExternalSetterExpressionHelper.cs index d4664b31a8d..e3b37d09c07 100644 --- a/src/HotChocolate/ApolloFederation/src/ApolloFederation/Resolvers/ExternalSetterExpressionHelper.cs +++ b/src/HotChocolate/ApolloFederation/src/ApolloFederation/Resolvers/ExternalSetterExpressionHelper.cs @@ -47,6 +47,30 @@ public static void TryAddExternalSetter(ObjectType type, ObjectTypeConfiguration } } + public static void TryAddExternalSetter(InterfaceType type, InterfaceTypeConfiguration typeDef) + { + List? block = null; + + foreach (var field in type.Fields) + { + if (field.Directives.ContainsDirective() + && typeDef.Fields.FirstOrDefault(f => f.Name == field.Name) is + { Member: PropertyInfo { SetMethod: not null } property }) + { + var expression = CreateTrySetValue(type.RuntimeType, property, field.Name); + (block ??= []).Add(expression); + } + } + + if (block is not null) + { + typeDef.Features.Set(new ExternalSetter( + Lambda>( + Block(block), s_type, s_data, s_entity) + .Compile())); + } + } + private static Expression CreateTrySetValue( Type runtimeType, PropertyInfo property, diff --git a/src/HotChocolate/ApolloFederation/src/ApolloFederation/Resolvers/ReferenceResolverArgumentExpressionBuilder.cs b/src/HotChocolate/ApolloFederation/src/ApolloFederation/Resolvers/ReferenceResolverArgumentExpressionBuilder.cs index dbc2c2d3273..c454efaa5b2 100644 --- a/src/HotChocolate/ApolloFederation/src/ApolloFederation/Resolvers/ReferenceResolverArgumentExpressionBuilder.cs +++ b/src/HotChocolate/ApolloFederation/src/ApolloFederation/Resolvers/ReferenceResolverArgumentExpressionBuilder.cs @@ -14,6 +14,14 @@ internal sealed class ReferenceResolverArgumentExpressionBuilder : nameof(ArgumentParser.GetValue), BindingFlags.Static | BindingFlags.Public)!; + private readonly Type _targetType; + + public ReferenceResolverArgumentExpressionBuilder(Type targetType) + { + ArgumentNullException.ThrowIfNull(targetType); + _targetType = targetType; + } + public override Expression Build(ParameterExpressionBuilderContext context) { var param = context.Parameter; @@ -35,7 +43,7 @@ public override Expression Build(ParameterExpressionBuilderContext context) param, typeKey, context.ResolverContext, - typeof(ObjectType)); + _targetType); var getValueMethod = _getValue.MakeGenericMethod(param.ParameterType); var getValue = Expression.Call( getValueMethod, diff --git a/src/HotChocolate/ApolloFederation/src/ApolloFederation/Resolvers/ReferenceResolverAttribute.cs b/src/HotChocolate/ApolloFederation/src/ApolloFederation/Resolvers/ReferenceResolverAttribute.cs index 0e22bb37a46..c2b9fea185f 100644 --- a/src/HotChocolate/ApolloFederation/src/ApolloFederation/Resolvers/ReferenceResolverAttribute.cs +++ b/src/HotChocolate/ApolloFederation/src/ApolloFederation/Resolvers/ReferenceResolverAttribute.cs @@ -11,6 +11,7 @@ namespace HotChocolate.ApolloFederation.Resolvers; /// [AttributeUsage( AttributeTargets.Class + | AttributeTargets.Interface | AttributeTargets.Struct | AttributeTargets.Method, AllowMultiple = true)] @@ -31,18 +32,32 @@ protected internal override void TryConfigure( { case Type type: OnConfigure(objectTypeDescriptor, type); - break; + return; case MethodInfo method: OnConfigure(objectTypeDescriptor, method); - break; + return; + } + } + + if (descriptor is IInterfaceTypeDescriptor interfaceTypeDescriptor) + { + switch (element) + { + case Type type: + OnConfigure(interfaceTypeDescriptor, type); + return; + + case MethodInfo method: + OnConfigure(interfaceTypeDescriptor, method); + return; } } } private void OnConfigure(IObjectTypeDescriptor descriptor, Type type) { - var entityResolverDescriptor = new EntityResolverDescriptor(descriptor); + var entityResolverDescriptor = new EntityResolverDescriptor.Object(descriptor); if (EntityResolverType is not null) { @@ -85,7 +100,56 @@ private void OnConfigure(IObjectTypeDescriptor descriptor, Type type) private static void OnConfigure(IObjectTypeDescriptor descriptor, MethodInfo method) { - var entityResolverDescriptor = new EntityResolverDescriptor(descriptor); + var entityResolverDescriptor = new EntityResolverDescriptor.Object(descriptor); + entityResolverDescriptor.ResolveReferenceWith(method); + } + + private void OnConfigure(IInterfaceTypeDescriptor descriptor, Type type) + { + var entityResolverDescriptor = new EntityResolverDescriptor.Interface(descriptor); + + if (EntityResolverType is not null) + { + if (EntityResolver is not null) + { + var method = EntityResolverType.GetMethod(EntityResolver); + + if (method is null) + { + throw ReferenceResolverAttribute_EntityResolverNotFound( + EntityResolverType, + EntityResolver); + } + + entityResolverDescriptor.ResolveReferenceWith(method); + } + else + { + entityResolverDescriptor.ResolveReferenceWith(EntityResolverType); + } + } + else if (EntityResolver is not null) + { + var method = type.GetMethod(EntityResolver); + + if (method is null) + { + throw ReferenceResolverAttribute_EntityResolverNotFound( + type, + EntityResolver); + } + + entityResolverDescriptor.ResolveReferenceWith(method); + } + else + { + entityResolverDescriptor.ResolveReferenceWith(type); + } + } + + private static void OnConfigure(IInterfaceTypeDescriptor descriptor, MethodInfo method) + { + var entityResolverDescriptor = new EntityResolverDescriptor.Interface(descriptor); entityResolverDescriptor.ResolveReferenceWith(method); } } diff --git a/src/HotChocolate/ApolloFederation/src/ApolloFederation/Types/Descriptors/EntityResolverDescriptor.Interface.cs b/src/HotChocolate/ApolloFederation/src/ApolloFederation/Types/Descriptors/EntityResolverDescriptor.Interface.cs new file mode 100644 index 00000000000..d90d2bda7b8 --- /dev/null +++ b/src/HotChocolate/ApolloFederation/src/ApolloFederation/Types/Descriptors/EntityResolverDescriptor.Interface.cs @@ -0,0 +1,120 @@ +using System.Linq.Expressions; +using System.Reflection; +using HotChocolate.ApolloFederation.Properties; +using HotChocolate.ApolloFederation.Resolvers; +using HotChocolate.Features; +using HotChocolate.Resolvers; +using HotChocolate.Types.Descriptors; +using HotChocolate.Types.Descriptors.Configurations; +using HotChocolate.Utilities; + +namespace HotChocolate.ApolloFederation.Types; + +public static partial class EntityResolverDescriptor +{ + public sealed class Interface + : DescriptorBase + , IEntityResolverDescriptor + , IEntityResolverDescriptor + { + private readonly IInterfaceTypeDescriptor _typeDescriptor; + + internal Interface( + IInterfaceTypeDescriptor descriptor) + : this((InterfaceTypeDescriptor)descriptor, typeof(TEntity)) + { + } + + internal Interface( + IInterfaceTypeDescriptor descriptor, + Type? entityType = null) + : base(descriptor.Extend().Context) + { + _typeDescriptor = descriptor; + + _typeDescriptor + .Extend() + .OnBeforeCreate(OnCompleteConfiguration); + + Configuration.EntityType = entityType; + } + + private void OnCompleteConfiguration(InterfaceTypeConfiguration typeConfiguration) + { + if (Configuration.Resolver is not null) + { + var resolvers = typeConfiguration.Features.GetOrSet>(); + resolvers.Add(Configuration.Resolver); + } + } + + /// + public IInterfaceTypeDescriptor ResolveReference( + FieldResolverDelegate fieldResolver) + => ResolveReference(fieldResolver, []); + + /// + public IInterfaceTypeDescriptor ResolveReferenceWith( + Expression> method) + => ResolveReferenceWith(method); + + /// + public IInterfaceTypeDescriptor ResolveReferenceWith( + Expression> method) + { + ArgumentNullException.ThrowIfNull(method); + + var member = method.TryExtractMember(true); + + if (member is MethodInfo m) + { + return ResolveReferenceWith(m); + } + + throw new ArgumentException( + FederationResources.EntityResolver_MustBeMethod, + nameof(member)); + } + + /// + public IInterfaceTypeDescriptor ResolveReferenceWith(MethodInfo method) + { + ArgumentNullException.ThrowIfNull(method); + + var argumentBuilder = new ReferenceResolverArgumentExpressionBuilder(typeof(InterfaceType)); + + var resolver = + Context.ResolverCompiler.CompileResolve( + method, + sourceType: typeof(object), + resolverType: method.DeclaringType ?? typeof(object), + parameterExpressionBuilders: [argumentBuilder]); + + return ResolveReference(resolver.Resolver!, argumentBuilder.Required); + } + + /// + public IInterfaceTypeDescriptor ResolveReferenceWith() + => ResolveReferenceWith(typeof(TResolver)); + + /// + public IInterfaceTypeDescriptor ResolveReferenceWith(Type type) + => ResolveReferenceWith( + Context.TypeInspector.GetNodeResolverMethod( + Configuration.EntityType ?? type, + type)!); + + private IInterfaceTypeDescriptor ResolveReference( + FieldResolverDelegate fieldResolver, + IReadOnlyList required) + { + ArgumentNullException.ThrowIfNull(fieldResolver); + ArgumentNullException.ThrowIfNull(required); + + Configuration.Resolver = new ReferenceResolverConfiguration(fieldResolver, required); + return _typeDescriptor; + } + + protected internal override EntityResolverConfiguration Configuration { get; protected set; } = new(); + } +} diff --git a/src/HotChocolate/ApolloFederation/src/ApolloFederation/Types/Descriptors/EntityResolverDescriptor.Object.cs b/src/HotChocolate/ApolloFederation/src/ApolloFederation/Types/Descriptors/EntityResolverDescriptor.Object.cs new file mode 100644 index 00000000000..d0f616ae943 --- /dev/null +++ b/src/HotChocolate/ApolloFederation/src/ApolloFederation/Types/Descriptors/EntityResolverDescriptor.Object.cs @@ -0,0 +1,120 @@ +using System.Linq.Expressions; +using System.Reflection; +using HotChocolate.ApolloFederation.Properties; +using HotChocolate.ApolloFederation.Resolvers; +using HotChocolate.Features; +using HotChocolate.Resolvers; +using HotChocolate.Types.Descriptors; +using HotChocolate.Types.Descriptors.Configurations; +using HotChocolate.Utilities; + +namespace HotChocolate.ApolloFederation.Types; + +public static partial class EntityResolverDescriptor +{ + public sealed class Object + : DescriptorBase + , IEntityResolverDescriptor + , IEntityResolverDescriptor + { + private readonly IObjectTypeDescriptor _typeDescriptor; + + internal Object( + IObjectTypeDescriptor descriptor) + : this((ObjectTypeDescriptor)descriptor, typeof(TEntity)) + { + } + + internal Object( + IObjectTypeDescriptor descriptor, + Type? entityType = null) + : base(descriptor.Extend().Context) + { + _typeDescriptor = descriptor; + + _typeDescriptor + .Extend() + .OnBeforeCreate(OnCompleteConfiguration); + + Configuration.EntityType = entityType; + } + + private void OnCompleteConfiguration(ObjectTypeConfiguration typeConfiguration) + { + if (Configuration.Resolver is not null) + { + var resolvers = typeConfiguration.Features.GetOrSet>(); + resolvers.Add(Configuration.Resolver); + } + } + + /// + public IObjectTypeDescriptor ResolveReference( + FieldResolverDelegate fieldResolver) + => ResolveReference(fieldResolver, []); + + /// + public IObjectTypeDescriptor ResolveReferenceWith( + Expression> method) + => ResolveReferenceWith(method); + + /// + public IObjectTypeDescriptor ResolveReferenceWith( + Expression> method) + { + ArgumentNullException.ThrowIfNull(method); + + var member = method.TryExtractMember(true); + + if (member is MethodInfo m) + { + return ResolveReferenceWith(m); + } + + throw new ArgumentException( + FederationResources.EntityResolver_MustBeMethod, + nameof(member)); + } + + /// + public IObjectTypeDescriptor ResolveReferenceWith(MethodInfo method) + { + ArgumentNullException.ThrowIfNull(method); + + var argumentBuilder = new ReferenceResolverArgumentExpressionBuilder(typeof(ObjectType)); + + var resolver = + Context.ResolverCompiler.CompileResolve( + method, + sourceType: typeof(object), + resolverType: method.DeclaringType ?? typeof(object), + parameterExpressionBuilders: [argumentBuilder]); + + return ResolveReference(resolver.Resolver!, argumentBuilder.Required); + } + + /// + public IObjectTypeDescriptor ResolveReferenceWith() + => ResolveReferenceWith(typeof(TResolver)); + + /// + public IObjectTypeDescriptor ResolveReferenceWith(Type type) + => ResolveReferenceWith( + Context.TypeInspector.GetNodeResolverMethod( + Configuration.EntityType ?? type, + type)!); + + private IObjectTypeDescriptor ResolveReference( + FieldResolverDelegate fieldResolver, + IReadOnlyList required) + { + ArgumentNullException.ThrowIfNull(fieldResolver); + ArgumentNullException.ThrowIfNull(required); + + Configuration.Resolver = new ReferenceResolverConfiguration(fieldResolver, required); + return _typeDescriptor; + } + + protected internal override EntityResolverConfiguration Configuration { get; protected set; } = new(); + } +} diff --git a/src/HotChocolate/ApolloFederation/src/ApolloFederation/Types/Descriptors/EntityResolverDescriptor.cs b/src/HotChocolate/ApolloFederation/src/ApolloFederation/Types/Descriptors/EntityResolverDescriptor.cs deleted file mode 100644 index bc80186f5da..00000000000 --- a/src/HotChocolate/ApolloFederation/src/ApolloFederation/Types/Descriptors/EntityResolverDescriptor.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System.Linq.Expressions; -using System.Reflection; -using HotChocolate.ApolloFederation.Properties; -using HotChocolate.ApolloFederation.Resolvers; -using HotChocolate.Features; -using HotChocolate.Resolvers; -using HotChocolate.Types.Descriptors; -using HotChocolate.Types.Descriptors.Configurations; -using HotChocolate.Utilities; - -namespace HotChocolate.ApolloFederation.Types; - -/// -/// The entity descriptor allows specifying a reference resolver. -/// -public sealed class EntityResolverDescriptor - : DescriptorBase - , IEntityResolverDescriptor - , IEntityResolverDescriptor -{ - private readonly IObjectTypeDescriptor _typeDescriptor; - - internal EntityResolverDescriptor( - IObjectTypeDescriptor descriptor) - : this((ObjectTypeDescriptor)descriptor, typeof(TEntity)) - { - } - - internal EntityResolverDescriptor( - IObjectTypeDescriptor descriptor, - Type? entityType = null) - : base(descriptor.Extend().Context) - { - _typeDescriptor = descriptor; - - _typeDescriptor - .Extend() - .OnBeforeCreate(OnCompleteConfiguration); - - Configuration.EntityType = entityType; - } - - private void OnCompleteConfiguration(ObjectTypeConfiguration typeConfiguration) - { - if (Configuration.Resolver is not null) - { - var resolvers = typeConfiguration.Features.GetOrSet>(); - resolvers.Add(Configuration.Resolver); - } - } - - /// - public IObjectTypeDescriptor ResolveReference( - FieldResolverDelegate fieldResolver) - => ResolveReference(fieldResolver, []); - - /// - public IObjectTypeDescriptor ResolveReferenceWith( - Expression> method) - => ResolveReferenceWith(method); - - /// - public IObjectTypeDescriptor ResolveReferenceWith( - Expression> method) - { - ArgumentNullException.ThrowIfNull(method); - - var member = method.TryExtractMember(true); - - if (member is MethodInfo m) - { - return ResolveReferenceWith(m); - } - - throw new ArgumentException( - FederationResources.EntityResolver_MustBeMethod, - nameof(member)); - } - - /// - public IObjectTypeDescriptor ResolveReferenceWith(MethodInfo method) - { - ArgumentNullException.ThrowIfNull(method); - - var argumentBuilder = new ReferenceResolverArgumentExpressionBuilder(); - - var resolver = - Context.ResolverCompiler.CompileResolve( - method, - sourceType: typeof(object), - resolverType: method.DeclaringType ?? typeof(object), - parameterExpressionBuilders: [argumentBuilder]); - - return ResolveReference(resolver.Resolver!, argumentBuilder.Required); - } - - /// - public IObjectTypeDescriptor ResolveReferenceWith() - => ResolveReferenceWith(typeof(TResolver)); - - /// - public IObjectTypeDescriptor ResolveReferenceWith(Type type) - => ResolveReferenceWith( - Context.TypeInspector.GetNodeResolverMethod( - Configuration.EntityType ?? type, - type)!); - - private IObjectTypeDescriptor ResolveReference( - FieldResolverDelegate fieldResolver, - IReadOnlyList required) - { - ArgumentNullException.ThrowIfNull(fieldResolver); - ArgumentNullException.ThrowIfNull(required); - - Configuration.Resolver = new ReferenceResolverConfiguration(fieldResolver, required); - return _typeDescriptor; - } - - protected internal override EntityResolverConfiguration Configuration { get; protected set; } = new(); -} diff --git a/src/HotChocolate/ApolloFederation/src/ApolloFederation/Types/Descriptors/IEntityResolverDescriptor.cs b/src/HotChocolate/ApolloFederation/src/ApolloFederation/Types/Descriptors/IEntityResolverDescriptor.cs index fe91fb190f6..a840def433d 100644 --- a/src/HotChocolate/ApolloFederation/src/ApolloFederation/Types/Descriptors/IEntityResolverDescriptor.cs +++ b/src/HotChocolate/ApolloFederation/src/ApolloFederation/Types/Descriptors/IEntityResolverDescriptor.cs @@ -6,7 +6,7 @@ namespace HotChocolate.ApolloFederation.Types; /// /// The entity descriptor allows to specify a reference resolver. /// -public interface IEntityResolverDescriptor +public interface IEntityResolverDescriptor { /// /// Resolve an entity from its representation. @@ -17,13 +17,13 @@ public interface IEntityResolverDescriptor /// /// Returns the descriptor for configuration chaining. /// - IObjectTypeDescriptor ResolveReferenceWith(MethodInfo method); + TTypeDescriptor ResolveReferenceWith(MethodInfo method); } /// /// The entity descriptor allows to specify a reference resolver. /// -public interface IEntityResolverDescriptor +public interface IEntityResolverDescriptor { /// /// Resolve an entity from its representation. @@ -34,7 +34,7 @@ public interface IEntityResolverDescriptor /// /// Returns the descriptor for configuration chaining. /// - IObjectTypeDescriptor ResolveReferenceWith( + TTypeDescriptor ResolveReferenceWith( Expression> method); /// @@ -46,5 +46,5 @@ IObjectTypeDescriptor ResolveReferenceWith( /// /// Returns the descriptor for configuration chaining. /// - IObjectTypeDescriptor ResolveReferenceWith(MethodInfo method); + TTypeDescriptor ResolveReferenceWith(MethodInfo method); } diff --git a/src/HotChocolate/ApolloFederation/src/ApolloFederation/Types/Directives/KeyDescriptorExtensions.cs b/src/HotChocolate/ApolloFederation/src/ApolloFederation/Types/Directives/KeyDescriptorExtensions.cs index 997c4b79b1f..d2f6034b406 100644 --- a/src/HotChocolate/ApolloFederation/src/ApolloFederation/Types/Directives/KeyDescriptorExtensions.cs +++ b/src/HotChocolate/ApolloFederation/src/ApolloFederation/Types/Directives/KeyDescriptorExtensions.cs @@ -40,7 +40,7 @@ public static class KeyDescriptorExtensions /// /// is null or . /// - public static IEntityResolverDescriptor Key( + public static IEntityResolverDescriptor Key( this IObjectTypeDescriptor descriptor, string fieldSet, bool resolvable = true) @@ -49,7 +49,7 @@ public static IEntityResolverDescriptor Key( ArgumentException.ThrowIfNullOrEmpty(fieldSet); descriptor.Directive(new KeyDirective(fieldSet, resolvable)); - return new EntityResolverDescriptor(descriptor); + return new EntityResolverDescriptor.Object(descriptor); } /// @@ -81,7 +81,7 @@ public static IEntityResolverDescriptor Key( /// /// is null or . /// - public static IEntityResolverDescriptor Key( + public static IEntityResolverDescriptor Key( this IObjectTypeDescriptor descriptor, string fieldSet, bool resolvable = true) @@ -91,7 +91,7 @@ public static IEntityResolverDescriptor Key( descriptor.Directive(new KeyDirective(fieldSet, resolvable)); - return new EntityResolverDescriptor(descriptor); + return new EntityResolverDescriptor.Object(descriptor); } /// @@ -132,7 +132,7 @@ public static IEntityResolverDescriptor Key( /// /// is null or . /// - public static IInterfaceTypeDescriptor Key( + public static IEntityResolverDescriptor Key( this IInterfaceTypeDescriptor descriptor, string fieldSet, bool resolvable = true) @@ -140,6 +140,49 @@ public static IInterfaceTypeDescriptor Key( ArgumentNullException.ThrowIfNull(descriptor); ArgumentException.ThrowIfNullOrEmpty(fieldSet); - return descriptor.Directive(new KeyDirective(fieldSet, resolvable)); + descriptor.Directive(new KeyDirective(fieldSet, resolvable)); + return new EntityResolverDescriptor.Interface(descriptor); + } + + /// + /// Adds the @key directive which is used to indicate a combination of fields that can be used to uniquely + /// identify and fetch an object or interface. The specified field set can represent single field (e.g. "id"), + /// multiple fields (e.g. "id name") or nested selection sets (e.g. "id user { name }"). Multiple keys can + /// be specified on a target type. + /// + /// type Foo @key(fields: "id") { + /// id: ID! + /// field: String + /// } + /// + /// + /// + /// The object type descriptor on which this directive shall be annotated. + /// + /// + /// The field set that describes the key. + /// Grammatically, a field set is a selection set minus the braces. + /// + /// + /// Boolean flag to indicate whether this entity is resolvable locally. + /// + /// + /// + /// is null. + /// + /// + /// is null or . + /// + public static IEntityResolverDescriptor Key( + this IInterfaceTypeDescriptor descriptor, + string fieldSet, + bool resolvable = true) + { + ArgumentNullException.ThrowIfNull(descriptor); + ArgumentException.ThrowIfNullOrEmpty(fieldSet); + + descriptor.Directive(new KeyDirective(fieldSet, resolvable)); + + return new EntityResolverDescriptor.Interface(descriptor); } } diff --git a/src/HotChocolate/ApolloFederation/test/ApolloFederation.Tests/EntitiesResolverForInterfaceTests.cs b/src/HotChocolate/ApolloFederation/test/ApolloFederation.Tests/EntitiesResolverForInterfaceTests.cs new file mode 100644 index 00000000000..97198a69d86 --- /dev/null +++ b/src/HotChocolate/ApolloFederation/test/ApolloFederation.Tests/EntitiesResolverForInterfaceTests.cs @@ -0,0 +1,545 @@ +using GreenDonut; +using HotChocolate.ApolloFederation.Resolvers; +using HotChocolate.ApolloFederation.Types; +using HotChocolate.Execution; +using HotChocolate.Language; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using static HotChocolate.ApolloFederation.TestHelper; + +namespace HotChocolate.ApolloFederation; + +public class EntitiesResolverForInterfaceTests +{ + [Fact] + public async Task TestResolveViaForeignServiceType() + { + // arrange + var schema = await new ServiceCollection() + .AddGraphQL() + .AddApolloFederation() + .AddQueryType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .BuildSchemaAsync(); + + var context = CreateResolverContext(schema); + + // act + var representations = RepresentationsOf( + nameof(IForeignType), + new + { + id = "1", + someExternalField = "someExternalField" + }); + var result = + await EntitiesResolver.ResolveAsync(schema, representations, context); + + // assert + var obj = Assert.IsAssignableFrom(result[0]); + Assert.Equal("1", obj.Id); + Assert.Equal("someExternalField", obj.SomeExternalField); + Assert.Equal("InternalValue", obj.InternalField); + } + + [Fact] + public async Task TestResolveViaForeignServiceType_MixedTypes() + { + // arrange + var schema = await new ServiceCollection() + .AddGraphQL() + .AddApolloFederation() + .AddQueryType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .BuildSchemaAsync(); + + var context = CreateResolverContext(schema); + + // act + var representations = new List + { + new("IMixedFieldTypes", + new ObjectValueNode( + new ObjectFieldNode("id", "1"), + new ObjectFieldNode("intField", 25))) + }; + + // assert + var result = + await EntitiesResolver.ResolveAsync(schema, representations, context); + var obj = Assert.IsAssignableFrom(result[0]); + Assert.Equal("1", obj.Id); + Assert.Equal(25, obj.IntField); + Assert.Equal("InternalValue", obj.InternalField); + } + + [Fact] + public async Task TestResolveViaEntityResolver() + { + var schema = await new ServiceCollection() + .AddGraphQL() + .AddApolloFederation() + .AddQueryType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .BuildSchemaAsync(); + + var context = CreateResolverContext(schema); + + // act + var representations = new List + { + new("ITypeWithReferenceResolver", + new ObjectValueNode(new ObjectFieldNode("Id", "1"))) + }; + + // assert + var result = await EntitiesResolver.ResolveAsync(schema, representations, context); + var obj = Assert.IsAssignableFrom(result[0]); + Assert.Equal("1", obj.Id); + Assert.Equal("SomeField", obj.SomeField); + } + + [Fact] + public async Task TestResolveViaEntityResolver_WithDataLoader() + { + // arrange + var schema = await new ServiceCollection() + .AddGraphQL() + .AddApolloFederation() + .AddQueryType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .BuildSchemaAsync(); + + var batchScheduler = new ManualBatchScheduler(); + var dataLoader = new IFederatedTypeDataLoader(batchScheduler, new DataLoaderOptions()); + + var serviceProviderMock = new Mock(); + serviceProviderMock.Setup(c => c.GetService(typeof(IFederatedTypeDataLoader))).Returns(dataLoader); + + var context = CreateResolverContext( + schema, + null, + mock => mock.Setup(c => c.Services).Returns(serviceProviderMock.Object)); + + var representations = RepresentationsOf( + nameof(FederatedType), + new { Id = "1" }, + new { Id = "2" }, + new { 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 Task TestResolveViaEntityResolver_NoTypeFound() + { + var schema = await new ServiceCollection() + .AddGraphQL() + .AddApolloFederation() + .AddQueryType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .BuildSchemaAsync(); + + var context = CreateResolverContext(schema); + + // act + var representations = new List + { + new("NonExistingTypeName", new ObjectValueNode()) + }; + + // assert + Task ShouldThrow() => EntitiesResolver.ResolveAsync(schema, representations, context); + await Assert.ThrowsAsync(ShouldThrow); + } + + [Fact] + public async Task TestResolveViaEntityResolver_NoResolverFound() + { + var schema = await new ServiceCollection() + .AddGraphQL() + .AddApolloFederation() + .AddQueryType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .BuildSchemaAsync(); + + var context = CreateResolverContext(schema); + + // act + var representations = new List + { + new("ITypeWithoutRefResolver", new ObjectValueNode()) + }; + + // assert + Task ShouldThrow() => EntitiesResolver.ResolveAsync(schema, representations, context); + await Assert.ThrowsAsync(ShouldThrow); + } + + [Fact] + public async Task TestDetailFieldResolver_Required() + { + var schema = await new ServiceCollection() + .AddGraphQL() + .AddApolloFederation() + .AddQueryType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .BuildSchemaAsync(); + + var context = CreateResolverContext(schema); + + var representations = new List + { + new("IFederatedTypeWithRequiredDetail", + new ObjectValueNode( + [ + new ObjectFieldNode("detail", + new ObjectValueNode([new ObjectFieldNode("id", "testId")])) + ])) + }; + + var result = await EntitiesResolver.ResolveAsync(schema, representations, context); + + var single = Assert.Single(result); + var obj = Assert.IsAssignableFrom(single); + + Assert.Equal("testId", obj.Id); + Assert.Equal("testId", obj.Detail.Id); + } + + [Fact] + public async Task TestDetailFieldResolver_Optional() + { + var schema = await new ServiceCollection() + .AddGraphQL() + .AddApolloFederation() + .AddQueryType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .BuildSchemaAsync(); + + var context = CreateResolverContext(schema); + + var representations = new List + { + new("IFederatedTypeWithOptionalDetail", + new ObjectValueNode( + [ + new ObjectFieldNode("detail", + new ObjectValueNode( + [ + new ObjectFieldNode("id", "testId") + ])) + ])) + }; + + var result = await EntitiesResolver.ResolveAsync(schema, representations, context); + + var single = Assert.Single(result); + var obj = Assert.IsAssignableFrom(single); + + Assert.Equal("testId", obj.Id); + Assert.Equal("testId", obj.Detail!.Id); + } + + public class Query + { + public IForeignType ForeignType { get; set; } = null!; + public ITypeWithReferenceResolver TypeWithReferenceResolver { get; set; } = null!; + public ITypeWithoutRefResolver TypeWithoutRefResolver { get; set; } = null!; + public IMixedFieldTypes MixedFieldTypes { get; set; } = null!; + public IFederatedType TypeWithReferenceResolverMany { get; set; } = null!; + } + + public interface ITypeWithoutRefResolver + { + string Id { get; set; } + } + + public class TypeWithoutRefResolver : ITypeWithoutRefResolver + { + public string Id { get; set; } = null!; + } + + [ReferenceResolver(EntityResolver = nameof(Get))] + public interface ITypeWithReferenceResolver + { + string Id { get; set; } + string SomeField { get; set; } + + public static ITypeWithReferenceResolver Get([LocalState] ObjectValueNode data) + { + return new TypeWithReferenceResolver { Id = "1", SomeField = "SomeField" }; + } + } + + [ReferenceResolver(EntityResolver = nameof(Get))] + public class TypeWithReferenceResolver : ITypeWithReferenceResolver + { + public string Id { get; set; } = null!; + public string SomeField { get; set; } = null!; + + public static TypeWithReferenceResolver Get([LocalState] ObjectValueNode data) + { + return new TypeWithReferenceResolver { Id = "1", SomeField = "SomeField" }; + } + } + + public interface IForeignType + { + [Key] + public string Id { get; } + + [External] + public string SomeExternalField { get; } + + public string InternalField => "InternalValue"; + + [ReferenceResolver] + public static IForeignType GetById(string id, string someExternalField) + => new ForeignType(id, someExternalField); + } + + public class ForeignType : IForeignType + { + public ForeignType(string id, string someExternalField) + { + Id = id; + SomeExternalField = someExternalField; + } + + [Key] + public string Id { get; } + + [External] + public string SomeExternalField { get; } + + public string InternalField => "InternalValue"; + + [ReferenceResolver] + public static ForeignType GetById(string id, string someExternalField) + => new(id, someExternalField); + } + + public interface IMixedFieldTypes + { + [Key] + public string Id { get; } + + [External] + public int IntField { get; } + + public string InternalField { get; set; } + + [ReferenceResolver] + public static IMixedFieldTypes GetByExternal(string id, int intField) + => new MixedFieldTypes(id, intField); + } + + [ExtendServiceType] + public class MixedFieldTypes : IMixedFieldTypes + { + public MixedFieldTypes(string id, int intField) + { + Id = id; + IntField = intField; + } + + [Key] + public string Id { get; } + + [External] + public int IntField { get; } + + public string InternalField { get; set; } = "InternalValue"; + + [ReferenceResolver] + public static MixedFieldTypes GetByExternal(string id, int intField) => new(id, intField); + } + + public interface IFederatedType + { + [Key] + string Id { get; set; } + + string SomeField { get; set; } + + [ReferenceResolver] + public static async Task GetById( + [LocalState] ObjectValueNode data, + [Service] IFederatedTypeDataLoader loader) + { + var id = + data.Fields.FirstOrDefault(_ => _.Name.Value == "Id")?.Value.Value?.ToString() ?? + string.Empty; + + return await loader.LoadAsync(id); + } + } + + public class FederatedType : IFederatedType + { + [Key] + public string Id { get; set; } = null!; + + public string SomeField { get; set; } = null!; + + [ReferenceResolver] + public static async Task GetById( + [LocalState] ObjectValueNode data, + [Service] IFederatedTypeDataLoader loader) + { + var id = + data.Fields.FirstOrDefault(_ => _.Name.Value == "Id")?.Value.Value?.ToString() ?? + string.Empty; + + return (FederatedType?)await loader.LoadAsync(id); + } + } + + public class IFederatedTypeDataLoader : BatchDataLoader + { + public int TimesCalled { get; private set; } + + public IFederatedTypeDataLoader( + IBatchScheduler batchScheduler, + DataLoaderOptions options) : 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); + } + } + + public interface IFederatedTypeWithRequiredDetail + { + string Id { get; set; } + + FederatedTypeDetail Detail { get; set; } + + [ReferenceResolver] + public static IFederatedTypeWithRequiredDetail ReferenceResolver([Map("detail.id")] string detailId) + => new FederatedTypeWithRequiredDetail() + { + Id = detailId, + Detail = new FederatedTypeDetail + { + Id = detailId + } + }; + } + + public class FederatedTypeWithRequiredDetail : IFederatedTypeWithRequiredDetail + { + public string Id { get; set; } = null!; + + public FederatedTypeDetail Detail { get; set; } = null!; + + [ReferenceResolver] + public static FederatedTypeWithRequiredDetail ReferenceResolver([Map("detail.id")] string detailId) + => new() + { + Id = detailId, + Detail = new FederatedTypeDetail + { + Id = detailId + } + }; + } + + public interface IFederatedTypeWithOptionalDetail + { + string Id { get; set; } + + FederatedTypeDetail? Detail { get; } + + [ReferenceResolver] + public static IFederatedTypeWithOptionalDetail ReferenceResolver([Map("detail.id")] string detailId) + => new FederatedTypeWithOptionalDetail() + { + Id = detailId, + Detail = new FederatedTypeDetail + { + Id = detailId + } + }; + } + + public class FederatedTypeWithOptionalDetail : IFederatedTypeWithOptionalDetail + { + public string Id { get; set; } = null!; + + public FederatedTypeDetail? Detail { get; set; } + + [ReferenceResolver] + public static FederatedTypeWithOptionalDetail ReferenceResolver([Map("detail.id")] string detailId) + => new() + { + Id = detailId, + Detail = new FederatedTypeDetail + { + Id = detailId + } + }; + } + + public class FederatedTypeDetail + { + public string Id { get; set; } = null!; + } +} diff --git a/src/HotChocolate/ApolloFederation/test/ApolloFederation.Tests/EntitiesResolverTests.cs b/src/HotChocolate/ApolloFederation/test/ApolloFederation.Tests/EntitiesResolverForObjectTests.cs similarity index 99% rename from src/HotChocolate/ApolloFederation/test/ApolloFederation.Tests/EntitiesResolverTests.cs rename to src/HotChocolate/ApolloFederation/test/ApolloFederation.Tests/EntitiesResolverForObjectTests.cs index ad1032ccf8e..cd4f538a3ff 100644 --- a/src/HotChocolate/ApolloFederation/test/ApolloFederation.Tests/EntitiesResolverTests.cs +++ b/src/HotChocolate/ApolloFederation/test/ApolloFederation.Tests/EntitiesResolverForObjectTests.cs @@ -9,7 +9,7 @@ namespace HotChocolate.ApolloFederation; -public class EntitiesResolverTests +public class EntitiesResolverForObjectTests { [Fact] public async Task TestResolveViaForeignServiceType() diff --git a/src/HotChocolate/ApolloFederation/test/ApolloFederation.Tests/TestHelper.cs b/src/HotChocolate/ApolloFederation/test/ApolloFederation.Tests/TestHelper.cs index ea6a4a224eb..da482027868 100644 --- a/src/HotChocolate/ApolloFederation/test/ApolloFederation.Tests/TestHelper.cs +++ b/src/HotChocolate/ApolloFederation/test/ApolloFederation.Tests/TestHelper.cs @@ -3,6 +3,7 @@ using HotChocolate.Language; using HotChocolate.Resolvers; using HotChocolate.Types; +using HotChocolate.Utilities; using Moq; namespace HotChocolate.ApolloFederation; @@ -24,6 +25,7 @@ public static IResolverContext CreateResolverContext( mock.Setup(c => c.Parent<_Service>()).Returns(new _Service()); mock.Setup(c => c.Clone()).Returns(mock.Object); mock.SetupGet(c => c.Schema).Returns(schema); + mock.Setup(c => c.Service()).Returns(new DefaultTypeConverter()); if (type is not null) {