From 5c419eb8d687d097484723b60a5cf9cfdd7b3324 Mon Sep 17 00:00:00 2001 From: PascalSenn Date: Sun, 18 Oct 2020 12:43:34 +0200 Subject: [PATCH] Convention Extensions (#2451) Co-authored-by: Michael Staib --- .../SchemaBuilderExtensions.Conventions.cs | 33 + .../Core/src/Types/InternalsVisibleTo.cs | 4 +- .../Core/src/Types/SchemaBuilder.cs | 13 +- .../Descriptors/Conventions/Convention.cs | 5 + .../Conventions/ConventionContext.cs | 18 +- .../Conventions/ConventionExtension.cs | 14 + .../Conventions/ConventionExtension`1.cs | 10 + .../Descriptors/Conventions/Convention`1.cs | 15 +- .../Conventions/DescriptorContext.cs | 106 ++- .../Conventions/IConventionContext.cs | 13 - .../Conventions/IConventionExtension.cs | 7 + .../Descriptors/Definitions/DefinitionBase.cs | 4 + .../Definitions/DefinitionBase~1.cs | 4 + .../Core/src/Types/Utilities/ThrowHelper.cs | 163 +++-- .../test/Types.Tests/SchemaBuilderTests.cs | 655 +++++++++++++++++- .../Descriptors/DescriptorContextTests.cs | 8 +- ...hould_Throw_When_DuplicatedConvention.snap | 3 + .../Filters/Convention/FilterConvention.cs | 72 +- .../Convention/FilterConventionDefinition.cs | 11 +- .../Convention/FilterConventionDescriptor.cs | 31 +- .../Convention/FilterConventionExtension.cs | 93 +++ .../Convention/FilterTypeInterceptor.cs | 12 +- .../Convention/IFilterConventionDescriptor.cs | 19 +- .../QueryableFilterProviderExtensions.cs | 18 + .../Data/Filters/Visitor/FilterProvider.cs | 30 +- .../Visitor/FilterProviderExtensions.cs | 69 ++ .../Visitor/IFilterProviderConvention.cs | 4 +- .../Visitor/IFilterProviderExtension.cs | 9 + .../Convention/ProjectionConvention.cs | 18 +- .../Convention/ProjectionProvider.cs | 28 +- .../Convention/ISortConventionDescriptor.cs | 17 + .../Data/Sorting/Convention/SortConvention.cs | 76 +- .../Convention/SortConventionDefinition.cs | 9 +- .../Convention/SortConventionDescriptor.cs | 14 + .../Convention/SortConventionExtension.cs | 110 +++ ...ConventionDescriptorQueryableExtensions.cs | 2 +- ...cs => QueryableDefaultSortFieldHandler.cs} | 2 +- .../QueryableSortProviderExtension.cs | 18 + .../Visitor/ISortProviderConvention.cs | 4 +- .../Sorting/Visitor/ISortProviderExtension.cs | 9 + .../src/Data/Sorting/Visitor/SortProvider.cs | 42 +- .../Sorting/Visitor/SortProviderDefiniton.cs | 4 +- .../Sorting/Visitor/SortProviderExtensions.cs | 74 ++ .../src/Data/Utilities/ExtensionHelpers.cs | 36 + .../Convention/FilterConventionTests.cs | 180 ++++- .../FilterConventionExtensionsTests.cs | 285 ++++++++ .../FilterProviderExtensionsTests.cs | 66 ++ .../FilterTypeAttributeTests.cs | 4 +- .../Convention/SortConventionTests.cs | 222 +++++- ..._When_OperationHandlerIsNotRegistered.snap | 2 +- .../SortConventionExtensionsTests.cs | 375 ++++++++++ .../SortProviderExtensionsTests.cs | 99 +++ .../SortTypeAttributeTests.cs | 8 +- 53 files changed, 2867 insertions(+), 280 deletions(-) create mode 100644 src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/ConventionExtension.cs create mode 100644 src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/ConventionExtension`1.cs create mode 100644 src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/IConventionExtension.cs create mode 100644 src/HotChocolate/Core/test/Types.Tests/__snapshots__/SchemaBuilderTests.Convention_Should_Throw_When_DuplicatedConvention.snap create mode 100644 src/HotChocolate/Data/src/Data/Filters/Convention/FilterConventionExtension.cs create mode 100644 src/HotChocolate/Data/src/Data/Filters/Expressions/QueryableFilterProviderExtensions.cs create mode 100644 src/HotChocolate/Data/src/Data/Filters/Visitor/FilterProviderExtensions.cs create mode 100644 src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterProviderExtension.cs create mode 100644 src/HotChocolate/Data/src/Data/Sorting/Convention/SortConventionExtension.cs rename src/HotChocolate/Data/src/Data/Sorting/Expressions/Handlers/{QueryableDefaultFieldHandler.cs => QueryableDefaultSortFieldHandler.cs} (98%) create mode 100644 src/HotChocolate/Data/src/Data/Sorting/Expressions/QueryableSortProviderExtension.cs create mode 100644 src/HotChocolate/Data/src/Data/Sorting/Visitor/ISortProviderExtension.cs create mode 100644 src/HotChocolate/Data/src/Data/Sorting/Visitor/SortProviderExtensions.cs create mode 100644 src/HotChocolate/Data/src/Data/Utilities/ExtensionHelpers.cs create mode 100644 src/HotChocolate/Data/test/Data.Filters.Tests/FilterConventionExtensionsTests.cs create mode 100644 src/HotChocolate/Data/test/Data.Filters.Tests/FilterProviderExtensionsTests.cs create mode 100644 src/HotChocolate/Data/test/Data.Sorting.Tests/SortConventionExtensionsTests.cs create mode 100644 src/HotChocolate/Data/test/Data.Sorting.Tests/SortProviderExtensionsTests.cs diff --git a/src/HotChocolate/Core/src/Types/Extensions/SchemaBuilderExtensions.Conventions.cs b/src/HotChocolate/Core/src/Types/Extensions/SchemaBuilderExtensions.Conventions.cs index 1f2ebbf54a3..dd2a1f87d9b 100644 --- a/src/HotChocolate/Core/src/Types/Extensions/SchemaBuilderExtensions.Conventions.cs +++ b/src/HotChocolate/Core/src/Types/Extensions/SchemaBuilderExtensions.Conventions.cs @@ -10,6 +10,32 @@ namespace HotChocolate { public static partial class SchemaBuilderExtensions { + public static ISchemaBuilder AddConvention( + this ISchemaBuilder builder, + Type type, + string? scope = null) + { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + + return builder.AddConvention(typeof(T), type, scope); + } + + public static ISchemaBuilder AddConvention( + this ISchemaBuilder builder, + CreateConvention conventionFactory, + string? scope = null) + { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + + return builder.AddConvention(typeof(T), conventionFactory, scope); + } + public static ISchemaBuilder AddConvention( this ISchemaBuilder builder, Type convention, @@ -231,6 +257,13 @@ public static partial class SchemaBuilderExtensions where T : IConvention => builder.TryAddConvention(typeof(T), conventionFactory, scope); + public static ISchemaBuilder TryAddConvention( + this ISchemaBuilder builder, + Type type, + string? scope = null) + where T : IConvention => + builder.TryAddConvention(typeof(T), type, scope); + public static ISchemaBuilder TryAddConvention( this ISchemaBuilder builder, IConvention convention, diff --git a/src/HotChocolate/Core/src/Types/InternalsVisibleTo.cs b/src/HotChocolate/Core/src/Types/InternalsVisibleTo.cs index 1982877fcb7..4e87382dc42 100644 --- a/src/HotChocolate/Core/src/Types/InternalsVisibleTo.cs +++ b/src/HotChocolate/Core/src/Types/InternalsVisibleTo.cs @@ -10,4 +10,6 @@ [assembly: InternalsVisibleTo("HotChocolate.Types.Selections")] [assembly: InternalsVisibleTo("HotChocolate.Types.Filters.Tests")] [assembly: InternalsVisibleTo("HotChocolate.Types.Sorting.Tests")] -[assembly: InternalsVisibleTo("HotChocolate.AspNetCore.Tests")] \ No newline at end of file +[assembly: InternalsVisibleTo("HotChocolate.AspNetCore.Tests")] +[assembly: InternalsVisibleTo("HotChocolate.Data.Filters.Tests")] +[assembly: InternalsVisibleTo("HotChocolate.Data.Sorting.Tests")] diff --git a/src/HotChocolate/Core/src/Types/SchemaBuilder.cs b/src/HotChocolate/Core/src/Types/SchemaBuilder.cs index 539e58529da..6e43965efb0 100644 --- a/src/HotChocolate/Core/src/Types/SchemaBuilder.cs +++ b/src/HotChocolate/Core/src/Types/SchemaBuilder.cs @@ -30,8 +30,8 @@ public partial class SchemaBuilder : ISchemaBuilder new Dictionary(); private readonly Dictionary _resolvers = new Dictionary(); - private readonly Dictionary<(Type, string), CreateConvention> _conventions = - new Dictionary<(Type, string), CreateConvention>(); + private readonly Dictionary<(Type, string), List> _conventions = + new Dictionary<(Type, string), List>(); private readonly Dictionary _clrTypes = new Dictionary(); private readonly List _schemaInterceptors = new List(); @@ -190,8 +190,13 @@ public ISchemaBuilder AddType(Type type) throw new ArgumentNullException(nameof(convention)); } - _conventions[(convention, scope)] = factory ?? - throw new ArgumentNullException(nameof(factory)); + if(!_conventions.TryGetValue((convention, scope), out List factories)) + { + factories = new List(); + _conventions[(convention, scope)] = factories; + } + + factories.Add(factory); return this; } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/Convention.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/Convention.cs index f1a5adf1c14..ad369000367 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/Convention.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/Convention.cs @@ -21,6 +21,7 @@ protected set throw new InvalidOperationException( "The convention scope is immutable."); } + _scope = value; } } @@ -32,6 +33,10 @@ protected internal virtual void Initialize(IConventionContext context) MarkInitialized(); } + protected internal virtual void OnComplete(IConventionContext context) + { + } + protected void MarkInitialized() { if (IsInitialized) diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/ConventionContext.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/ConventionContext.cs index 628b10eb5c7..1b5f6bf16ab 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/ConventionContext.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/ConventionContext.cs @@ -8,20 +8,15 @@ namespace HotChocolate.Types.Descriptors internal sealed class ConventionContext : IConventionContext { public ConventionContext( - IConvention convention, string? scope, IServiceProvider services, IDescriptorContext descriptorContext) { - Convention = convention; Scope = scope; Services = services; DescriptorContext = descriptorContext; } - /// - public IConvention Convention { get; } - /// public string? Scope { get; } @@ -34,10 +29,13 @@ internal sealed class ConventionContext : IConventionContext /// public IDescriptorContext DescriptorContext { get; } - /// - public void ReportError(ISchemaError error) - { - throw new NotImplementedException(); - } + public static ConventionContext Create( + string? scope, + IServiceProvider services, + IDescriptorContext descriptorContext) => + new ConventionContext( + scope, + services, + descriptorContext); } } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/ConventionExtension.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/ConventionExtension.cs new file mode 100644 index 00000000000..5c44449582d --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/ConventionExtension.cs @@ -0,0 +1,14 @@ +namespace HotChocolate.Types.Descriptors +{ + public abstract class ConventionExtension + : Convention + , IConventionExtension + { + public abstract void Merge(IConventionContext context, Convention convention); + + protected internal sealed override void OnComplete(IConventionContext context) + { + base.OnComplete(context); + } + } +} diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/ConventionExtension`1.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/ConventionExtension`1.cs new file mode 100644 index 00000000000..0b46757c3d6 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/ConventionExtension`1.cs @@ -0,0 +1,10 @@ +namespace HotChocolate.Types.Descriptors +{ + public abstract class ConventionExtension + : Convention, + IConventionExtension + where TDefinition : class + { + public abstract void Merge(IConventionContext context, Convention convention); + } +} diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/Convention`1.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/Convention`1.cs index 888550672c4..ee8e70b85b1 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/Convention`1.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/Convention`1.cs @@ -10,21 +10,26 @@ public abstract class Convention : Convention { private TDefinition? _definition; + protected virtual TDefinition? Definition + { + get + { + return _definition; + } + } + protected internal sealed override void Initialize(IConventionContext context) { AssertUninitialized(); Scope = context.Scope; _definition = CreateDefinition(context); - - OnComplete(context, _definition); - MarkInitialized(); - _definition = null; } - protected virtual void OnComplete(IConventionContext context, TDefinition definition) + protected internal override void OnComplete(IConventionContext context) { + _definition = null; } protected abstract TDefinition CreateDefinition(IConventionContext context); diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/DescriptorContext.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/DescriptorContext.cs index 71682a5d38c..ad59a9139d0 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/DescriptorContext.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/DescriptorContext.cs @@ -12,7 +12,10 @@ public sealed class DescriptorContext : IDescriptorContext { private readonly Dictionary<(Type, string?), IConvention> _conventions = new Dictionary<(Type, string?), IConvention>(); - private readonly IReadOnlyDictionary<(Type, string?), CreateConvention> _convFactories; + + private readonly IReadOnlyDictionary<(Type, string?), List> + _convFactories; + private readonly IServiceProvider _services; private INamingConventions? _naming; @@ -22,7 +25,7 @@ public sealed class DescriptorContext : IDescriptorContext private DescriptorContext( IReadOnlySchemaOptions options, - IReadOnlyDictionary<(Type, string?), CreateConvention> convFactories, + IReadOnlyDictionary<(Type, string?), List> convFactories, IServiceProvider services, IDictionary contextData, SchemaBuilder.LazySchema schema, @@ -57,6 +60,7 @@ public INamingConventions Naming _naming = GetConventionOrDefault( () => new DefaultNamingConventions(Options.UseXmlDocumentation)); } + return _naming; } } @@ -70,6 +74,7 @@ public ITypeInspector TypeInspector _inspector = this.GetConventionOrDefault( new DefaultTypeInspector()); } + return _inspector; } } @@ -90,61 +95,96 @@ public ITypeInspector TypeInspector throw new ArgumentNullException(nameof(defaultConvention)); } - if (!TryGetConvention(scope, out T? convention)) + if (_conventions.TryGetValue((typeof(T), scope), out IConvention? conv) && + conv is T castedConvention) + { + return castedConvention; + } + + CreateConventions( + scope, + out IConvention? createdConvention, + out IList? extensions); + + createdConvention ??= createdConvention as T; + createdConvention ??= _services.GetService(typeof(T)) as T; + createdConvention ??= defaultConvention(); + + if (createdConvention is Convention init) { - convention = _services.GetService(typeof(T)) as T; + ConventionContext conventionContext = + ConventionContext.Create(scope, _services, this); + + init.Initialize(conventionContext); + MergeExtensions(conventionContext, init, extensions); + init.OnComplete(conventionContext); } - if (convention is null) + if (createdConvention is T createdConventionOfT) { - convention = defaultConvention(); + _conventions[(typeof(T), scope)] = createdConventionOfT; + return createdConventionOfT; } - return convention; + throw ThrowHelper.Convention_ConventionCouldNotBeCreated(typeof(T), scope); } - private bool TryGetConvention( + private void CreateConventions( string? scope, - [NotNullWhen(true)] out T? convention) - where T : class, IConvention + out IConvention? createdConvention, + out IList extensions) { - if (_conventions.TryGetValue( - (typeof(T), scope), out IConvention? conv)) - { - if (conv is T casted) - { - convention = casted; - return true; - } - } + createdConvention = null; + extensions = new List(); if (_convFactories.TryGetValue( (typeof(T), scope), - out CreateConvention? factory)) + out List? factories)) { - conv = factory(_services); - if (conv is Convention init) + for (var i = 0; i < factories.Count; i++) { - var conventionContext = new ConventionContext(init, scope, _services, this); - init.Initialize(conventionContext); - _conventions[(typeof(T), scope)] = init; + IConvention conv = factories[i](_services); + if (conv is IConventionExtension extension) + { + extensions.Add(extension); + } + else + { + if (createdConvention is not null) + { + throw ThrowHelper.Convention_TwoConventionsRegisteredForScope( + typeof(T), + createdConvention, + conv, + scope); + } + + createdConvention = conv; + } } + } + } - if (conv is T casted) + private static void MergeExtensions( + IConventionContext context, + Convention convention, + IList extensions) + { + for (var m = 0; m < extensions.Count; m++) + { + if (extensions[m] is Convention extensionConvention) { - convention = casted; - return true; + extensionConvention.Initialize(context); + extensions[m].Merge(context, convention); + extensionConvention.OnComplete(context); } } - - convention = default; - return false; } internal static DescriptorContext Create( IReadOnlySchemaOptions? options = null, IServiceProvider? services = null, - IReadOnlyDictionary<(Type, string?), CreateConvention>? conventions = null, + IReadOnlyDictionary<(Type, string?), List>? conventions = null, IDictionary? contextData = null, SchemaBuilder.LazySchema? schema = null, ISchemaInterceptor? schemaInterceptor = null, @@ -152,7 +192,7 @@ public ITypeInspector TypeInspector { return new DescriptorContext( options ?? new SchemaOptions(), - conventions ?? new Dictionary<(Type, string?), CreateConvention>(), + conventions ?? new Dictionary<(Type, string?), List>(), services ?? new EmptyServiceProvider(), contextData ?? new Dictionary(), schema ?? new SchemaBuilder.LazySchema(), diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/IConventionContext.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/IConventionContext.cs index df5d65e5681..91f859801a1 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/IConventionContext.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/IConventionContext.cs @@ -10,11 +10,6 @@ namespace HotChocolate.Types.Descriptors /// public interface IConventionContext : IHasScope { - /// - /// The convention that is being initialized. - /// - IConvention Convention { get; } - /// /// The schema level services. /// @@ -31,13 +26,5 @@ public interface IConventionContext : IHasScope /// The descriptor context that is passed through the initialization process. /// IDescriptorContext DescriptorContext { get; } - - /// - /// Report a schema initialization error. - /// - /// - /// The error that occurred during initialization. - /// - void ReportError(ISchemaError error); } } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/IConventionExtension.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/IConventionExtension.cs new file mode 100644 index 00000000000..219407bdc8d --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/IConventionExtension.cs @@ -0,0 +1,7 @@ +namespace HotChocolate.Types.Descriptors +{ + public interface IConventionExtension : IConvention + { + void Merge(IConventionContext context, Convention convention); + } +} diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/DefinitionBase.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/DefinitionBase.cs index 44e02f393f7..342e5a23760 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/DefinitionBase.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/DefinitionBase.cs @@ -4,6 +4,10 @@ namespace HotChocolate.Types.Descriptors.Definitions { + /// + /// A type system definition is used in the type initialization to store properties + /// of a type system object. + /// public class DefinitionBase { protected DefinitionBase() { } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/DefinitionBase~1.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/DefinitionBase~1.cs index a9273ca0249..0aa3823bf74 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/DefinitionBase~1.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/DefinitionBase~1.cs @@ -2,6 +2,10 @@ namespace HotChocolate.Types.Descriptors.Definitions { + /// + /// A type system definition is used in the type initialization to store properties + /// of a type system object. + /// public class DefinitionBase : DefinitionBase , IHasSyntaxNode diff --git a/src/HotChocolate/Core/src/Types/Utilities/ThrowHelper.cs b/src/HotChocolate/Core/src/Types/Utilities/ThrowHelper.cs index caf1105377d..4ef32b1663d 100644 --- a/src/HotChocolate/Core/src/Types/Utilities/ThrowHelper.cs +++ b/src/HotChocolate/Core/src/Types/Utilities/ThrowHelper.cs @@ -15,61 +15,69 @@ internal static class ThrowHelper parameterName); public static GraphQLException EventMessage_InvalidCast( - Type expectedType, Type messageType) => - new GraphQLException(ErrorBuilder.New() - .SetMessage( - "The event message is of the type `{0}` and cannot be casted to `{1}.`", - messageType.FullName, - expectedType.FullName) - .Build()); + Type expectedType, + Type messageType) => + new GraphQLException( + ErrorBuilder.New() + .SetMessage( + "The event message is of the type `{0}` and cannot be casted to `{1}.`", + messageType.FullName, + expectedType.FullName) + .Build()); public static GraphQLException EventMessage_NotFound() => - new GraphQLException(ErrorBuilder.New() - .SetMessage("There is no event message on the context.") - .Build()); + new GraphQLException( + ErrorBuilder.New() + .SetMessage("There is no event message on the context.") + .Build()); public static SchemaException SubscribeAttribute_MessageTypeUnspecified( MemberInfo member) => - new SchemaException(SchemaErrorBuilder.New() - .SetMessage( - "You need to specify the message type on {0}.{1}. (SubscribeAttribute)", - member.DeclaringType.FullName, - member.Name) - .SetExtension("member", member) - .Build()); + new SchemaException( + SchemaErrorBuilder.New() + .SetMessage( + "You need to specify the message type on {0}.{1}. (SubscribeAttribute)", + member.DeclaringType.FullName, + member.Name) + .SetExtension("member", member) + .Build()); public static SchemaException SubscribeAttribute_TopicTypeUnspecified( MemberInfo member) => - new SchemaException(SchemaErrorBuilder.New() - .SetMessage( - "You need to specify the topic type on {0}.{1}. (SubscribeAttribute)", - member.DeclaringType.FullName, - member.Name) - .SetExtension("member", member) - .Build()); + new SchemaException( + SchemaErrorBuilder.New() + .SetMessage( + "You need to specify the topic type on {0}.{1}. (SubscribeAttribute)", + member.DeclaringType.FullName, + member.Name) + .SetExtension("member", member) + .Build()); public static SchemaException SubscribeAttribute_TopicOnParameterAndMethod( MemberInfo member) => - new SchemaException(SchemaErrorBuilder.New() - .SetMessage( - "The topic is declared multiple times on {0}.{1}. (TopicAttribute)", - member.DeclaringType!.FullName, - member.Name) - .SetExtension("member", member) - .Build()); + new SchemaException( + SchemaErrorBuilder.New() + .SetMessage( + "The topic is declared multiple times on {0}.{1}. (TopicAttribute)", + member.DeclaringType!.FullName, + member.Name) + .SetExtension("member", member) + .Build()); public static SchemaException SubscribeAttribute_SubscribeResolverNotFound( - MemberInfo member, string subscribeResolverName) => - new SchemaException(SchemaErrorBuilder.New() - .SetMessage( - "Unable to find the subscribe resolver `{2}` defined on {0}.{1}. " + - "The subscribe resolver bust be a method that is public, non-static " + - "and on the same type as the resolver. (SubscribeAttribute)", - member.DeclaringType!.FullName, - member.Name, - subscribeResolverName) - .SetExtension("member", member) - .Build()); + MemberInfo member, + string subscribeResolverName) => + new SchemaException( + SchemaErrorBuilder.New() + .SetMessage( + "Unable to find the subscribe resolver `{2}` defined on {0}.{1}. " + + "The subscribe resolver bust be a method that is public, non-static " + + "and on the same type as the resolver. (SubscribeAttribute)", + member.DeclaringType!.FullName, + member.Name, + subscribeResolverName) + .SetExtension("member", member) + .Build()); public static SchemaException Convention_UnableToCreateConvention( Type convention) => @@ -116,32 +124,65 @@ internal static class ThrowHelper public static SchemaException TypeInitializer_DuplicateTypeName( ITypeSystemObject type, ITypeSystemObject otherType) => - new SchemaException(SchemaErrorBuilder.New() - .SetMessage( - TypeResources.TypeInitializer_CompleteName_Duplicate, - type.Name) - .SetTypeSystemObject(type) - .SetExtension(nameof(otherType), otherType) - .Build()); + new SchemaException( + SchemaErrorBuilder.New() + .SetMessage( + TypeResources.TypeInitializer_CompleteName_Duplicate, + type.Name) + .SetTypeSystemObject(type) + .SetExtension(nameof(otherType), otherType) + .Build()); public static SchemaException NodeAttribute_NodeResolverNotFound( Type type, string nodeResolver) => - new SchemaException(SchemaErrorBuilder.New() - .SetMessage( - "The specified node resolver `{0}` does not exist on `{1}`.", - type.FullName ?? type.Name, - nodeResolver) - .Build()); + new SchemaException( + SchemaErrorBuilder.New() + .SetMessage( + "The specified node resolver `{0}` does not exist on `{1}`.", + type.FullName ?? type.Name, + nodeResolver) + .Build()); public static SchemaException NodeAttribute_IdFieldNotFound( Type type, string idField) => - new SchemaException(SchemaErrorBuilder.New() - .SetMessage( - "The specified id field `{0}` does not exist on `{1}`.", - type.FullName ?? type.Name, - idField) - .Build()); + new SchemaException( + SchemaErrorBuilder.New() + .SetMessage( + "The specified id field `{0}` does not exist on `{1}`.", + type.FullName ?? type.Name, + idField) + .Build()); + +#nullable enable + public static SchemaException Convention_TwoConventionsRegisteredForScope( + Type conventionType, + IConvention first, + IConvention other, + string? scope) => + new SchemaException( + SchemaErrorBuilder.New() + .SetMessage( + "There are two conventions registered for {0} in scope {1}. Only one " + + "convention is allowed. Use convention extensions if additional " + + "configuration is needed. Colliding conventions are {2} and {3}", + conventionType.FullName ?? conventionType.Name, + scope ?? "default", + first.GetType().FullName ?? first.GetType().Name, + other.GetType().FullName ?? other.GetType().Name) + .Build()); + + public static SchemaException Convention_ConventionCouldNotBeCreated( + Type conventionType, + string? scope) => + new SchemaException( + SchemaErrorBuilder.New() + .SetMessage( + "Convention of type {0} in scope {1} could not be created", + conventionType.FullName ?? conventionType.Name, + scope ?? "default") + .Build()); +#nullable disable } } diff --git a/src/HotChocolate/Core/test/Types.Tests/SchemaBuilderTests.cs b/src/HotChocolate/Core/test/Types.Tests/SchemaBuilderTests.cs index c16324fa5e4..ad90baf5c11 100644 --- a/src/HotChocolate/Core/test/Types.Tests/SchemaBuilderTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/SchemaBuilderTests.cs @@ -1368,30 +1368,6 @@ public void AddConvention_NoImplementation() } - [Fact] - public void AddConvention_Override_TypeOverType() - { - // arrange - // act - ISchema schema = SchemaBuilder.New() - .AddConvention(typeof(ITestConvention), typeof(TestConvention)) - .AddConvention(typeof(ITestConvention), typeof(TestConvention2)) - .AddType() - .AddQueryType(d => d - .Name("Query") - .Field("foo") - .Resolver("bar")) - .Create(); - - // assert - var testType = schema.GetType("ConventionTestType"); - var convention = testType.Context.GetConventionOrDefault( - new TestConvention()); - Assert.NotNull(convention); - Assert.IsType(convention); - } - - [Fact] public void AddConvention_ServiceDependency() { @@ -1589,6 +1565,633 @@ public void UseStateAndDelayedConfiguration() .MatchSnapshot(); } + [Fact] + public void Convention_Should_AddConvention_When_CalledWithInstance() + { + // arrange + var convention = new MockConvention(); + IDescriptorContext context = null!; + + // act + SchemaBuilder.New() + .AddQueryType( + d => d + .Name("Query") + .Field("foo") + .Resolver("bar") + .Extend().OnBeforeCreate( + (ctx, def) => + { + context = ctx; + })) + .AddConvention(convention) + .Create(); + IMockConvention result = context.GetConventionOrDefault( + () => throw new InvalidOperationException()); + + // assert + Assert.Equal(convention, result); + } + + [Fact] + public void Convention_Should_AddConvention_When_CalledWithGeneric() + { + // arrange + IDescriptorContext context = null!; + + // act + SchemaBuilder.New() + .AddQueryType( + d => d + .Name("Query") + .Field("foo") + .Resolver("bar") + .Extend().OnBeforeCreate( + (ctx, def) => + { + context = ctx; + })) + .AddConvention() + .Create(); + IMockConvention result = context.GetConventionOrDefault( + () => throw new InvalidOperationException()); + + // assert + Assert.IsType(result); + } + + [Fact] + public void Convention_Should_AddConvention_When_CalledWithFactory() + { + // arrange + var convention = new MockConvention(); + IDescriptorContext context = null!; + + // act + SchemaBuilder.New() + .AddQueryType( + d => d + .Name("Query") + .Field("foo") + .Resolver("bar") + .Extend().OnBeforeCreate( + (ctx, def) => + { + context = ctx; + })) + .AddConvention(sp => convention) + .Create(); + IMockConvention result = context.GetConventionOrDefault( + () => throw new InvalidOperationException()); + + // assert + Assert.Equal(convention, result); + } + + [Fact] + public void Convention_Should_AddConvention_When_CalledWithType() + { + // arrange + IDescriptorContext context = null!; + + // act + SchemaBuilder.New() + .AddQueryType( + d => d + .Name("Query") + .Field("foo") + .Resolver("bar") + .Extend().OnBeforeCreate( + (ctx, def) => + { + context = ctx; + })) + .AddConvention(typeof(MockConvention)) + .Create(); + IMockConvention result = context.GetConventionOrDefault( + () => throw new InvalidOperationException()); + + // assert + Assert.IsType(result); + } + + [Fact] + public void Convention_Should_TryAddConvention_When_CalledWithInstance() + { + // arrange + var convention = new MockConvention(); + IDescriptorContext context = null!; + + // act + SchemaBuilder.New() + .AddQueryType( + d => d + .Name("Query") + .Field("foo") + .Resolver("bar") + .Extend().OnBeforeCreate( + (ctx, def) => + { + context = ctx; + })) + .TryAddConvention(convention) + .Create(); + IMockConvention result = context.GetConventionOrDefault( + () => throw new InvalidOperationException()); + + // assert + Assert.Equal(convention, result); + } + + [Fact] + public void Convention_Should_TryAddConvention_When_CalledWithGeneric() + { + // arrange + IDescriptorContext context = null!; + + // act + SchemaBuilder.New() + .AddQueryType( + d => d + .Name("Query") + .Field("foo") + .Resolver("bar") + .Extend().OnBeforeCreate( + (ctx, def) => + { + context = ctx; + })) + .TryAddConvention() + .Create(); + IMockConvention result = context.GetConventionOrDefault( + () => throw new InvalidOperationException()); + + // assert + Assert.IsType(result); + } + + [Fact] + public void Convention_Should_TryAddConvention_When_CalledWithFactory() + { + // arrange + var convention = new MockConvention(); + IDescriptorContext context = null!; + + // act + SchemaBuilder.New() + .AddQueryType( + d => d + .Name("Query") + .Field("foo") + .Resolver("bar") + .Extend().OnBeforeCreate( + (ctx, def) => + { + context = ctx; + })) + .TryAddConvention(sp => convention) + .Create(); + IMockConvention result = context.GetConventionOrDefault( + () => throw new InvalidOperationException()); + + // assert + Assert.Equal(convention, result); + } + + [Fact] + public void Convention_Should_TryAddConvention_When_CalledWithType() + { + // arrange + IDescriptorContext context = null!; + + // act + SchemaBuilder.New() + .AddQueryType( + d => d + .Name("Query") + .Field("foo") + .Resolver("bar") + .Extend().OnBeforeCreate( + (ctx, def) => + { + context = ctx; + })) + .TryAddConvention(typeof(MockConvention)) + .Create(); + IMockConvention result = context.GetConventionOrDefault( + () => throw new InvalidOperationException()); + + // assert + Assert.IsType(result); + } + + [Fact] + public void Convention_Should_AddConventionType_When_CalledWithInstance() + { + // arrange + var convention = new MockConvention(); + IDescriptorContext context = null!; + + // act + SchemaBuilder.New() + .AddQueryType( + d => d + .Name("Query") + .Field("foo") + .Resolver("bar") + .Extend().OnBeforeCreate( + (ctx, def) => + { + context = ctx; + })) + .AddConvention(typeof(IMockConvention), convention) + .Create(); + IMockConvention result = context.GetConventionOrDefault( + () => throw new InvalidOperationException()); + + // assert + Assert.Equal(convention, result); + } + + [Fact] + public void Convention_Should_AddConventionType_When_CalledWithFactory() + { + // arrange + var convention = new MockConvention(); + IDescriptorContext context = null!; + + // act + SchemaBuilder.New() + .AddQueryType( + d => d + .Name("Query") + .Field("foo") + .Resolver("bar") + .Extend().OnBeforeCreate( + (ctx, def) => + { + context = ctx; + })) + .AddConvention(typeof(IMockConvention),sp => convention) + .Create(); + IMockConvention result = context.GetConventionOrDefault( + () => throw new InvalidOperationException()); + + // assert + Assert.Equal(convention, result); + } + + [Fact] + public void Convention_Should_AddConventionType_When_CalledWithType() + { + // arrange + IDescriptorContext context = null!; + + // act + SchemaBuilder.New() + .AddQueryType( + d => d + .Name("Query") + .Field("foo") + .Resolver("bar") + .Extend().OnBeforeCreate( + (ctx, def) => + { + context = ctx; + })) + .AddConvention(typeof(IMockConvention),typeof(MockConvention)) + .Create(); + IMockConvention result = context.GetConventionOrDefault( + () => throw new InvalidOperationException()); + + // assert + Assert.IsType(result); + } + + [Fact] + public void Convention_Should_TryAddConventionType_When_CalledWithInstance() + { + // arrange + var convention = new MockConvention(); + IDescriptorContext context = null!; + + // act + SchemaBuilder.New() + .AddQueryType( + d => d + .Name("Query") + .Field("foo") + .Resolver("bar") + .Extend().OnBeforeCreate( + (ctx, def) => + { + context = ctx; + })) + .TryAddConvention(typeof(IMockConvention),convention) + .Create(); + IMockConvention result = context.GetConventionOrDefault( + () => throw new InvalidOperationException()); + + // assert + Assert.Equal(convention, result); + } + + [Fact] + public void Convention_Should_TryAddConventionType_When_CalledWithFactory() + { + // arrange + var convention = new MockConvention(); + IDescriptorContext context = null!; + + // act + SchemaBuilder.New() + .AddQueryType( + d => d + .Name("Query") + .Field("foo") + .Resolver("bar") + .Extend().OnBeforeCreate( + (ctx, def) => + { + context = ctx; + })) + .TryAddConvention(typeof(IMockConvention), sp => convention) + .Create(); + IMockConvention result = context.GetConventionOrDefault( + () => throw new InvalidOperationException()); + + // assert + Assert.Equal(convention, result); + } + + [Fact] + public void Convention_Should_TryAddConventionType_When_CalledWithType() + { + // arrange + IDescriptorContext context = null!; + + // act + SchemaBuilder.New() + .AddQueryType( + d => d + .Name("Query") + .Field("foo") + .Resolver("bar") + .Extend().OnBeforeCreate( + (ctx, def) => + { + context = ctx; + })) + .TryAddConvention(typeof(MockConvention)) + .Create(); + IMockConvention result = context.GetConventionOrDefault( + () => throw new InvalidOperationException()); + + // assert + Assert.IsType(result); + } + + [Fact] + public void ConventionExtension_Should_AddConvention_When_CalledWithInstance() + { + // arrange + var convention = new MockConvention(); + IDescriptorContext context = null!; + + // act + SchemaBuilder.New() + .AddQueryType( + d => d + .Name("Query") + .Field("foo") + .Resolver("bar") + .Extend().OnBeforeCreate( + (ctx, def) => + { + context = ctx; + })) + .AddConvention(convention) + .AddConvention(new MockConventionExtension()) + .Create(); + IMockConvention result = context.GetConventionOrDefault( + () => throw new InvalidOperationException()); + + // assert + Assert.True(convention.IsExtended); + } + + [Fact] + public void ConventionExtension_Should_AddConvention_When_CalledWithGeneric() + { + // arrange + IDescriptorContext context = null!; + + // act + SchemaBuilder.New() + .AddQueryType( + d => d + .Name("Query") + .Field("foo") + .Resolver("bar") + .Extend().OnBeforeCreate( + (ctx, def) => + { + context = ctx; + })) + .AddConvention() + .AddConvention() + .Create(); + IMockConvention result = context.GetConventionOrDefault( + () => throw new InvalidOperationException()); + + // assert + Assert.True(Assert.IsType(result).IsExtended); + } + + [Fact] + public void ConventionExtension_Should_AddConvention_When_CalledWithFactory() + { + // arrange + var convention = new MockConvention(); + IDescriptorContext context = null!; + + // act + SchemaBuilder.New() + .AddQueryType( + d => d + .Name("Query") + .Field("foo") + .Resolver("bar") + .Extend().OnBeforeCreate( + (ctx, def) => + { + context = ctx; + })) + .AddConvention(sp => convention) + .AddConvention(sp => new MockConventionExtension()) + .Create(); + IMockConvention result = context.GetConventionOrDefault( + () => throw new InvalidOperationException()); + + // assert + Assert.True(Assert.IsType(result).IsExtended); + } + + [Fact] + public void ConventionExtension_Should_AddConvention_When_CalledWithType() + { + // arrange + IDescriptorContext context = null!; + + // act + SchemaBuilder.New() + .AddQueryType( + d => d + .Name("Query") + .Field("foo") + .Resolver("bar") + .Extend().OnBeforeCreate( + (ctx, def) => + { + context = ctx; + })) + .AddConvention(typeof(MockConvention)) + .AddConvention(typeof(MockConventionExtension)) + .Create(); + IMockConvention result = context.GetConventionOrDefault( + () => throw new InvalidOperationException()); + + // assert + Assert.True(Assert.IsType(result).IsExtended); + } + + [Fact] + public void Convention_Should_Throw_When_DuplicatedConvention() + { + // arrange + IDescriptorContext context = null!; + + // act + SchemaBuilder.New() + .AddQueryType( + d => d + .Name("Query") + .Field("foo") + .Resolver("bar") + .Extend().OnBeforeCreate( + (ctx, def) => + { + context = ctx; + })) + .AddConvention(typeof(MockConvention)) + .AddConvention(typeof(MockConvention)) + .Create(); + + // assert + SchemaException schemaException = Assert.Throws( + () => context.GetConventionOrDefault( + () => throw new InvalidOperationException())); + schemaException.Message.MatchSnapshot(); + } + + [Fact] + public void Convention_Should_UseDefault_When_NotRegistered() + { + // arrange + var convention = new MockConvention(); + IDescriptorContext context = null!; + + // act + SchemaBuilder.New() + .AddQueryType( + d => d + .Name("Query") + .Field("foo") + .Resolver("bar") + .Extend().OnBeforeCreate( + (ctx, def) => + { + context = ctx; + })) + .Create(); + IMockConvention result = context.GetConventionOrDefault( + () => convention); + + // assert + Assert.Equal(convention, result); + } + + [Fact] + public void Convention_Should_UseDefault_When_NotRegisteredAndApplyExtensions() + { + // arrange + var convention = new MockConvention(); + IDescriptorContext context = null!; + + // act + SchemaBuilder.New() + .AddQueryType( + d => d + .Name("Query") + .Field("foo") + .Resolver("bar") + .Extend() + .OnBeforeCreate( + (ctx, def) => + { + context = ctx; + })) + .AddConvention(typeof(MockConventionExtension)) + .Create(); + IMockConvention result = context.GetConventionOrDefault( + () => convention); + + // assert + Assert.Equal(result, convention); + Assert.True(Assert.IsType(result).IsExtended); + } + + public interface IMockConvention : IConvention + { + } + + public class MockConventionDefinition + { + public bool IsExtended { get; set; } + } + + public class MockConvention : Convention, IMockConvention + { + public bool IsExtended { get; set; } + public new MockConventionDefinition Definition => base.Definition; + protected override MockConventionDefinition CreateDefinition(IConventionContext context) + { + return new MockConventionDefinition(); + } + + protected internal override void OnComplete(IConventionContext context) + { + IsExtended = Definition.IsExtended; + base.OnComplete(context); + } + } + + public class MockConventionExtension : ConventionExtension + { + protected override MockConventionDefinition CreateDefinition(IConventionContext context) + { + return new MockConventionDefinition(); + } + + public override void Merge(IConventionContext context, Convention convention) + { + if (convention is MockConvention mockConvention) + { + mockConvention.Definition.IsExtended = true; + } + } + } + public class DynamicFooType : ObjectType { @@ -1760,7 +2363,7 @@ public class MyInterceptor } } - public class DummySchemaInterceptor : SchemaInterceptor + public class DummySchemaInterceptor : SchemaInterceptor { private readonly Action _onBeforeCreate; @@ -1770,7 +2373,7 @@ public DummySchemaInterceptor(Action onBeforeCreate) } public override void OnBeforeCreate( - IDescriptorContext context, + IDescriptorContext context, ISchemaBuilder schemaBuilder) => _onBeforeCreate(context); } diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Descriptors/DescriptorContextTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Descriptors/DescriptorContextTests.cs index 66694064952..33ce788eeb5 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Descriptors/DescriptorContextTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Descriptors/DescriptorContextTests.cs @@ -14,7 +14,7 @@ public void Create_With_Custom_NamingConventions() // arrange var options = new SchemaOptions(); var naming = new DefaultNamingConventions(); - var conventions = new Dictionary<(Type, string), CreateConvention>(); + var conventions = new Dictionary<(Type, string), List>(); var services = new DictionaryServiceProvider( typeof(INamingConventions), naming); @@ -42,9 +42,9 @@ public void Create_With_Custom_NamingConventions_AsIConvention() // arrange var options = new SchemaOptions(); var naming = new DefaultNamingConventions(); - var conventions = new Dictionary<(Type, string), CreateConvention> + var conventions = new Dictionary<(Type, string), List> { - { (typeof(INamingConventions), null), s => naming } + { (typeof(INamingConventions), null), new List{s => naming} } }; // act @@ -69,7 +69,7 @@ public void Create_With_Custom_TypeInspector() // arrange var options = new SchemaOptions(); var inspector = new DefaultTypeInspector(); - var conventions = new Dictionary<(Type, string), CreateConvention>(); + var conventions = new Dictionary<(Type, string), List>(); var services = new DictionaryServiceProvider( typeof(ITypeInspector), inspector); diff --git a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SchemaBuilderTests.Convention_Should_Throw_When_DuplicatedConvention.snap b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SchemaBuilderTests.Convention_Should_Throw_When_DuplicatedConvention.snap new file mode 100644 index 00000000000..bc1e9dd4ba5 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SchemaBuilderTests.Convention_Should_Throw_When_DuplicatedConvention.snap @@ -0,0 +1,3 @@ +For more details look at the `Errors` property. + +1. There are two conventions registered for HotChocolate.SchemaBuilderTests+IMockConvention in scope default. Only one convention is allowed. Use convention extensions if additional configuration is needed. Colliding conventions are HotChocolate.SchemaBuilderTests+MockConvention and HotChocolate.SchemaBuilderTests+MockConvention diff --git a/src/HotChocolate/Data/src/Data/Filters/Convention/FilterConvention.cs b/src/HotChocolate/Data/src/Data/Filters/Convention/FilterConvention.cs index 45e8861a7b9..779af57e33f 100644 --- a/src/HotChocolate/Data/src/Data/Filters/Convention/FilterConvention.cs +++ b/src/HotChocolate/Data/src/Data/Filters/Convention/FilterConvention.cs @@ -45,6 +45,8 @@ public FilterConvention(Action configure) throw new ArgumentNullException(nameof(configure)); } + internal new FilterConventionDefinition? Definition => base.Definition; + protected override FilterConventionDefinition CreateDefinition( IConventionContext context) { @@ -67,40 +69,46 @@ protected virtual void Configure(IFilterConventionDescriptor descriptor) { } - protected override void OnComplete( - IConventionContext context, - FilterConventionDefinition definition) + protected override void OnComplete(IConventionContext context) { - if (definition.Provider is null) + if (Definition?.Provider is null) { - throw FilterConvention_NoProviderFound(GetType(), definition.Scope); + throw FilterConvention_NoProviderFound(GetType(), Definition?.Scope); } - if (definition.ProviderInstance is null) + if (Definition.ProviderInstance is null) { _provider = - context.Services.GetOrCreateService(definition.Provider) ?? - throw FilterConvention_NoProviderFound(GetType(), definition.Scope); + context.Services.GetOrCreateService(Definition.Provider) ?? + throw FilterConvention_NoProviderFound(GetType(), Definition.Scope); } else { - _provider = definition.ProviderInstance; + _provider = Definition.ProviderInstance; } _namingConventions = context.DescriptorContext.Naming; - _operations = definition.Operations.ToDictionary( + _operations = Definition.Operations.ToDictionary( x => x.Id, FilterOperation.FromDefinition); - _bindings = definition.Bindings; - _configs = definition.Configurations; - _argumentName = definition.ArgumentName; + _bindings = Definition.Bindings; + _configs = Definition.Configurations; + _argumentName = Definition.ArgumentName; if (_provider is IFilterProviderConvention init) { + IReadOnlyList extensions = + CollectExtensions(context.Services, Definition); init.Initialize(context); + MergeExtensions(context, init, extensions); + init.OnComplete(context); } _typeInspector = context.DescriptorContext.TypeInspector; + + // It is important to always call base to continue the cleanup and the disposal of the + // definition + base.OnComplete(context); } @@ -255,5 +263,43 @@ public NameString GetOperationName(int operation) type = null; return false; } + + private static IReadOnlyList CollectExtensions( + IServiceProvider serviceProvider, + FilterConventionDefinition definition) + { + List extensions = new List(); + extensions.AddRange(definition.ProviderExtensions); + foreach (var extensionType in definition.ProviderExtensionsTypes) + { + if (serviceProvider.TryGetOrCreateService( + extensionType, + out var createdExtension)) + { + extensions.Add(createdExtension); + } + } + + return extensions; + } + + private static void MergeExtensions( + IConventionContext context, + IFilterProviderConvention provider, + IReadOnlyList extensions) + { + if (provider is Convention providerConvention) + { + for (var m = 0; m < extensions.Count; m++) + { + if (extensions[m] is IFilterProviderConvention extensionConvention) + { + extensionConvention.Initialize(context); + extensions[m].Merge(context, providerConvention); + extensionConvention.OnComplete(context); + } + } + } + } } } diff --git a/src/HotChocolate/Data/src/Data/Filters/Convention/FilterConventionDefinition.cs b/src/HotChocolate/Data/src/Data/Filters/Convention/FilterConventionDefinition.cs index 567586850d6..69a441509d3 100644 --- a/src/HotChocolate/Data/src/Data/Filters/Convention/FilterConventionDefinition.cs +++ b/src/HotChocolate/Data/src/Data/Filters/Convention/FilterConventionDefinition.cs @@ -7,15 +7,17 @@ namespace HotChocolate.Data.Filters { public class FilterConventionDefinition : IHasScope { + public static readonly string DefaultArgumentName = "where"; + public string? Scope { get; set; } - public string ArgumentName { get; set; } = "where"; + public string ArgumentName { get; set; } = DefaultArgumentName; public Type? Provider { get; set; } public IFilterProvider? ProviderInstance { get; set; } - public IList Operations { get; } = + public List Operations { get; } = new List(); public IDictionary Bindings { get; } = new Dictionary(); @@ -23,5 +25,10 @@ public class FilterConventionDefinition : IHasScope public IDictionary> Configurations { get; } = new Dictionary>( TypeReferenceComparer.Default); + + public List ProviderExtensions { get; } = + new List(); + + public List ProviderExtensionsTypes { get; } = new List(); } } diff --git a/src/HotChocolate/Data/src/Data/Filters/Convention/FilterConventionDescriptor.cs b/src/HotChocolate/Data/src/Data/Filters/Convention/FilterConventionDescriptor.cs index 1ba60a802e3..640e7291ea6 100644 --- a/src/HotChocolate/Data/src/Data/Filters/Convention/FilterConventionDescriptor.cs +++ b/src/HotChocolate/Data/src/Data/Filters/Convention/FilterConventionDescriptor.cs @@ -83,7 +83,10 @@ public IFilterConventionDescriptor BindRuntimeType(Type runtimeType, Type filter ConfigureFilterInputType configure) where TFilterType : FilterInputType => Configure( - Context.TypeInspector.GetTypeRef(typeof(TFilterType), TypeContext.Input, Definition.Scope), + Context.TypeInspector.GetTypeRef( + typeof(TFilterType), + TypeContext.Input, + Definition.Scope), configure); /// @@ -91,12 +94,16 @@ public IFilterConventionDescriptor BindRuntimeType(Type runtimeType, Type filter ConfigureFilterInputType configure) where TFilterType : FilterInputType => Configure( - Context.TypeInspector.GetTypeRef(typeof(TFilterType), TypeContext.Input, Definition.Scope), + Context.TypeInspector.GetTypeRef( + typeof(TFilterType), + TypeContext.Input, + Definition.Scope), d => { - configure.Invoke(FilterInputTypeDescriptor.From( - (FilterInputTypeDescriptor)d, - Definition.Scope)); + configure.Invoke( + FilterInputTypeDescriptor.From( + (FilterInputTypeDescriptor)d, + Definition.Scope)); }); protected IFilterConventionDescriptor Configure( @@ -155,6 +162,20 @@ public IFilterConventionDescriptor ArgumentName(NameString argumentName) return this; } + public IFilterConventionDescriptor AddProviderExtension() + where TExtension : class, IFilterProviderExtension + { + Definition.ProviderExtensionsTypes.Add(typeof(TExtension)); + return this; + } + + public IFilterConventionDescriptor AddProviderExtension(TExtension provider) + where TExtension : class, IFilterProviderExtension + { + Definition.ProviderExtensions.Add(provider); + return this; + } + /// /// Creates a new descriptor for /// diff --git a/src/HotChocolate/Data/src/Data/Filters/Convention/FilterConventionExtension.cs b/src/HotChocolate/Data/src/Data/Filters/Convention/FilterConventionExtension.cs new file mode 100644 index 00000000000..b126955cd29 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Filters/Convention/FilterConventionExtension.cs @@ -0,0 +1,93 @@ +using System; +using HotChocolate.Data.Utilities; +using HotChocolate.Types.Descriptors; +using static HotChocolate.Data.DataResources; + +namespace HotChocolate.Data.Filters +{ + /// + /// The filter convention extension can be used to extend a convention. + /// + public class FilterConventionExtension + : ConventionExtension + { + private Action? _configure; + + protected FilterConventionExtension() + { + _configure = Configure; + } + + public FilterConventionExtension(Action configure) + { + _configure = configure ?? + throw new ArgumentNullException(nameof(configure)); + } + + protected override FilterConventionDefinition CreateDefinition( + IConventionContext context) + { + if (_configure is null) + { + throw new InvalidOperationException(FilterConvention_NoConfigurationSpecified); + } + + var descriptor = FilterConventionDescriptor.New( + context.DescriptorContext, + context.Scope); + + _configure(descriptor); + _configure = null; + + return descriptor.CreateDefinition(); + } + + protected internal new void Initialize(IConventionContext context) + { + base.Initialize(context); + } + + protected virtual void Configure(IFilterConventionDescriptor descriptor) + { + } + + public override void Merge(IConventionContext context, Convention convention) + { + if (convention is FilterConvention filterConvention && + Definition is not null && + filterConvention.Definition is not null) + { + ExtensionHelpers.MergeDictionary( + Definition.Bindings, + filterConvention.Definition.Bindings); + + ExtensionHelpers.MergeListDictionary( + Definition.Configurations, + filterConvention.Definition.Configurations); + + filterConvention.Definition.Operations.AddRange(Definition.Operations); + + filterConvention.Definition.ProviderExtensions.AddRange( + Definition.ProviderExtensions); + + filterConvention.Definition.ProviderExtensionsTypes.AddRange( + Definition.ProviderExtensionsTypes); + + if (Definition.ArgumentName != FilterConventionDefinition.DefaultArgumentName) + { + filterConvention.Definition.ArgumentName = Definition.ArgumentName; + } + + if (Definition.Provider is not null) + { + filterConvention.Definition.Provider = Definition.Provider; + } + + if (Definition.ProviderInstance is not null) + { + filterConvention.Definition.ProviderInstance = Definition.ProviderInstance; + } + } + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Filters/Convention/FilterTypeInterceptor.cs b/src/HotChocolate/Data/src/Data/Filters/Convention/FilterTypeInterceptor.cs index 5ed9580d8c3..b9a0610a7bd 100644 --- a/src/HotChocolate/Data/src/Data/Filters/Convention/FilterTypeInterceptor.cs +++ b/src/HotChocolate/Data/src/Data/Filters/Convention/FilterTypeInterceptor.cs @@ -16,12 +16,12 @@ public class FilterTypeInterceptor public override void OnBeforeRegisterDependencies( ITypeDiscoveryContext discoveryContext, - DefinitionBase definition, - IDictionary contextData) + DefinitionBase? definition, + IDictionary contextData) { if (definition is FilterInputTypeDefinition def) { - IFilterConvention? convention = GetConvention( + IFilterConvention convention = GetConvention( discoveryContext.DescriptorContext, def.Scope); @@ -69,13 +69,13 @@ public class FilterTypeInterceptor public override void OnBeforeCompleteName( ITypeCompletionContext completionContext, - DefinitionBase definition, - IDictionary contextData) + DefinitionBase? definition, + IDictionary contextData) { if (definition is FilterInputTypeDefinition def && def.Scope != null) { - definition.Name = completionContext?.Scope + + definition.Name = completionContext.Scope + "_" + definition.Name; } diff --git a/src/HotChocolate/Data/src/Data/Filters/Convention/IFilterConventionDescriptor.cs b/src/HotChocolate/Data/src/Data/Filters/Convention/IFilterConventionDescriptor.cs index 9df27c751fe..954db465554 100644 --- a/src/HotChocolate/Data/src/Data/Filters/Convention/IFilterConventionDescriptor.cs +++ b/src/HotChocolate/Data/src/Data/Filters/Convention/IFilterConventionDescriptor.cs @@ -33,7 +33,7 @@ public interface IFilterConventionDescriptor /// The runtime type. /// GraphQL filter type. IFilterConventionDescriptor BindRuntimeType(Type runtimeType, Type filterType); - + /// /// Provides additional configuration for a filter type. /// @@ -94,5 +94,22 @@ IFilterConventionDescriptor Provider(TProvider provider) /// . /// IFilterConventionDescriptor ArgumentName(NameString argumentName); + + /// + /// Add a extensions that is applied to + /// + /// The filter provider extension type. + IFilterConventionDescriptor AddProviderExtension() + where TExtension : class, IFilterProviderExtension; + + /// + /// Add a extensions that is applied to + /// + /// + /// The concrete filter provider extension that shall be used. + /// + /// The filter provider extension type. + IFilterConventionDescriptor AddProviderExtension(TExtension provider) + where TExtension : class, IFilterProviderExtension; } } diff --git a/src/HotChocolate/Data/src/Data/Filters/Expressions/QueryableFilterProviderExtensions.cs b/src/HotChocolate/Data/src/Data/Filters/Expressions/QueryableFilterProviderExtensions.cs new file mode 100644 index 00000000000..7bc5151195f --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Filters/Expressions/QueryableFilterProviderExtensions.cs @@ -0,0 +1,18 @@ +using System; + +namespace HotChocolate.Data.Filters.Expressions +{ + public class QueryableFilterProviderExtensions + : FilterProviderExtensions + { + public QueryableFilterProviderExtensions() + { + } + + public QueryableFilterProviderExtensions( + Action> configure) + : base(configure) + { + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Filters/Visitor/FilterProvider.cs b/src/HotChocolate/Data/src/Data/Filters/Visitor/FilterProvider.cs index 9fd3debaff6..5450bc55d7a 100644 --- a/src/HotChocolate/Data/src/Data/Filters/Visitor/FilterProvider.cs +++ b/src/HotChocolate/Data/src/Data/Filters/Visitor/FilterProvider.cs @@ -32,12 +32,9 @@ public FilterProvider(Action> configure) throw new ArgumentNullException(nameof(configure)); } - public IReadOnlyCollection FieldHandlers => _fieldHandlers; + internal new FilterProviderDefinition? Definition => base.Definition; - public void Initialize(IConventionContext context) - { - base.Initialize(context); - } + public IReadOnlyCollection FieldHandlers => _fieldHandlers; protected override FilterProviderDefinition CreateDefinition(IConventionContext context) { @@ -54,11 +51,19 @@ protected override FilterProviderDefinition CreateDefinition(IConventionContext return descriptor.CreateDefinition(); } - protected override void OnComplete( - IConventionContext context, - FilterProviderDefinition definition) + void IFilterProviderConvention.Initialize(IConventionContext context) { - if (definition.Handlers.Count == 0) + base.Initialize(context); + } + + void IFilterProviderConvention.OnComplete(IConventionContext context) + { + OnComplete(context); + } + + protected override void OnComplete(IConventionContext context) + { + if (Definition.Handlers.Count == 0) { throw FilterProvider_NoHandlersConfigured(this); } @@ -70,7 +75,7 @@ protected override FilterProviderDefinition CreateDefinition(IConventionContext (typeof(ITypeInspector), context.DescriptorContext.TypeInspector)) .Include(context.Services); - foreach ((Type Type, IFilterFieldHandler? Instance) handler in definition.Handlers) + foreach ((Type Type, IFilterFieldHandler? Instance) handler in Definition.Handlers) { switch (handler.Instance) { @@ -79,10 +84,11 @@ protected override FilterProviderDefinition CreateDefinition(IConventionContext out IFilterFieldHandler? service): _fieldHandlers.Add(service); break; + case null: - context.ReportError( + throw new SchemaException( FilterProvider_UnableToCreateFieldHandler(this, handler.Type)); - break; + case IFilterFieldHandler casted: _fieldHandlers.Add(casted); break; diff --git a/src/HotChocolate/Data/src/Data/Filters/Visitor/FilterProviderExtensions.cs b/src/HotChocolate/Data/src/Data/Filters/Visitor/FilterProviderExtensions.cs new file mode 100644 index 00000000000..33c04e530f6 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Filters/Visitor/FilterProviderExtensions.cs @@ -0,0 +1,69 @@ +using System; +using HotChocolate.Types.Descriptors; +using static HotChocolate.Data.DataResources; + +namespace HotChocolate.Data.Filters +{ + public abstract class FilterProviderExtensions + : ConventionExtension, + IFilterProviderExtension, + IFilterProviderConvention + where TContext : IFilterVisitorContext + { + private Action>? _configure; + + protected FilterProviderExtensions() + { + _configure = Configure; + } + + public FilterProviderExtensions(Action> configure) + { + _configure = configure ?? + throw new ArgumentNullException(nameof(configure)); + } + + void IFilterProviderConvention.Initialize(IConventionContext context) + { + base.Initialize(context); + } + + void IFilterProviderConvention.OnComplete(IConventionContext context) + { + OnComplete(context); + } + + protected override FilterProviderDefinition CreateDefinition(IConventionContext context) + { + if (_configure is null) + { + throw new InvalidOperationException(FilterProvider_NoConfigurationSpecified); + } + + var descriptor = FilterProviderDescriptor.New(); + + _configure(descriptor); + _configure = null; + + return descriptor.CreateDefinition(); + } + + protected virtual void Configure(IFilterProviderDescriptor descriptor) { } + + public override void Merge(IConventionContext context, Convention convention) + { + if (Definition is not null && + convention is FilterProvider conv && + conv.Definition is {} target) + { + // Provider extensions should be applied by default before the default handlers, as + // the interceptor picks up the first handler. A provider extension should adds more + // specific handlers than the default providers + for (var i = Definition.Handlers.Count - 1; i >= 0; i--) + { + target.Handlers.Insert(0, Definition.Handlers[i]); + } + } + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterProviderConvention.cs b/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterProviderConvention.cs index 8e9a12ccf2d..3ee18dd9f58 100644 --- a/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterProviderConvention.cs +++ b/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterProviderConvention.cs @@ -4,6 +4,8 @@ namespace HotChocolate.Data.Filters { internal interface IFilterProviderConvention { - void Initialize(IConventionContext context); + internal void Initialize(IConventionContext context); + + internal void OnComplete(IConventionContext context); } } diff --git a/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterProviderExtension.cs b/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterProviderExtension.cs new file mode 100644 index 00000000000..c9173f1db57 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterProviderExtension.cs @@ -0,0 +1,9 @@ +using HotChocolate.Types.Descriptors; + +namespace HotChocolate.Data.Filters +{ + public interface IFilterProviderExtension + : IConventionExtension + { + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Convention/ProjectionConvention.cs b/src/HotChocolate/Data/src/Data/Projections/Convention/ProjectionConvention.cs index 93e453ac71e..b74f6b333c6 100644 --- a/src/HotChocolate/Data/src/Data/Projections/Convention/ProjectionConvention.cs +++ b/src/HotChocolate/Data/src/Data/Projections/Convention/ProjectionConvention.cs @@ -3,7 +3,7 @@ using HotChocolate.Resolvers; using HotChocolate.Types.Descriptors; using HotChocolate.Utilities; -using static HotChocolate.Data.ThrowHelper; +using static HotChocolate.Data.ThrowHelper; namespace HotChocolate.Data.Projections { @@ -51,24 +51,22 @@ protected virtual void Configure(IProjectionConventionDescriptor descriptor) { } - protected override void OnComplete( - IConventionContext context, - ProjectionConventionDefinition definition) + protected override void OnComplete(IConventionContext context) { - if (definition.Provider is null) + if (Definition.Provider is null) { - throw ProjectionConvention_NoProviderFound(GetType(), definition.Scope); + throw ProjectionConvention_NoProviderFound(GetType(), Definition.Scope); } - if (definition.ProviderInstance is null) + if (Definition.ProviderInstance is null) { _provider = - context.Services.GetOrCreateService(definition.Provider) ?? - throw ProjectionConvention_NoProviderFound(GetType(), definition.Scope); + context.Services.GetOrCreateService(Definition.Provider) ?? + throw ProjectionConvention_NoProviderFound(GetType(), Definition.Scope); } else { - _provider = definition.ProviderInstance; + _provider = Definition.ProviderInstance; } if (_provider is IProjectionProviderConvention init) diff --git a/src/HotChocolate/Data/src/Data/Projections/Convention/ProjectionProvider.cs b/src/HotChocolate/Data/src/Data/Projections/Convention/ProjectionProvider.cs index c5997e8182b..33eb4351a21 100644 --- a/src/HotChocolate/Data/src/Data/Projections/Convention/ProjectionProvider.cs +++ b/src/HotChocolate/Data/src/Data/Projections/Convention/ProjectionProvider.cs @@ -63,23 +63,20 @@ protected virtual void Configure(IProjectionProviderDescriptor descriptor) { } - protected override void OnComplete( - IConventionContext context, - ProjectionProviderDefinition definition) + protected override void OnComplete(IConventionContext context) { - if (definition.Handlers.Count == 0) + if (Definition.Handlers.Count == 0) { throw ProjectionProvider_NoHandlersConfigured(this); } IServiceProvider services = new DictionaryServiceProvider( (typeof(IConventionContext), context), - (typeof(IProjectionProvider), context.Convention), (typeof(IDescriptorContext), context.DescriptorContext), (typeof(ITypeInspector), context.DescriptorContext.TypeInspector)) .Include(context.Services); - foreach ((Type type, IProjectionFieldHandler? instance) in definition.Handlers) + foreach ((Type type, IProjectionFieldHandler? instance) in Definition.Handlers) { switch (instance) { @@ -88,17 +85,18 @@ protected virtual void Configure(IProjectionProviderDescriptor descriptor) out IProjectionFieldHandler? service): _fieldHandlers.Add(service); break; + case null: - context.ReportError( + throw new SchemaException( ProjectionConvention_UnableToCreateFieldHandler(this, type)); - break; + default: _fieldHandlers.Add(instance); break; } } - foreach ((var type, IProjectionFieldInterceptor? instance) in definition.Interceptors) + foreach ((var type, IProjectionFieldInterceptor? instance) in Definition.Interceptors) { switch (instance) { @@ -107,17 +105,18 @@ protected virtual void Configure(IProjectionProviderDescriptor descriptor) out IProjectionFieldInterceptor? service): _fieldInterceptors.Add(service); break; + case null: - context.ReportError( + throw new SchemaException( ProjectionConvention_UnableToCreateFieldHandler(this, type)); - break; + default: _fieldInterceptors.Add(instance); break; } } - foreach ((var type, IProjectionOptimizer? instance) in definition.Optimizers) + foreach ((var type, IProjectionOptimizer? instance) in Definition.Optimizers) { switch (instance) { @@ -126,10 +125,11 @@ protected virtual void Configure(IProjectionProviderDescriptor descriptor) out IProjectionOptimizer? service): _optimizer.Add(service); break; + case null: - context.ReportError( + throw new SchemaException( ProjectionConvention_UnableToCreateFieldHandler(this, type)); - break; + default: _optimizer.Add(instance); break; diff --git a/src/HotChocolate/Data/src/Data/Sorting/Convention/ISortConventionDescriptor.cs b/src/HotChocolate/Data/src/Data/Sorting/Convention/ISortConventionDescriptor.cs index e3881b45402..be4ed086a55 100644 --- a/src/HotChocolate/Data/src/Data/Sorting/Convention/ISortConventionDescriptor.cs +++ b/src/HotChocolate/Data/src/Data/Sorting/Convention/ISortConventionDescriptor.cs @@ -113,5 +113,22 @@ ISortConventionDescriptor Provider(TProvider provider) /// . /// ISortConventionDescriptor ArgumentName(NameString argumentName); + + /// + /// Add a extensions that is applied to + /// + /// The sort provider extension type. + ISortConventionDescriptor AddProviderExtension() + where TExtension : class, ISortProviderExtension; + + /// + /// Add a extensions that is applied to + /// + /// + /// The concrete sort provider extension that shall be used. + /// + /// The sort provider extension type. + ISortConventionDescriptor AddProviderExtension(TExtension provider) + where TExtension : class, ISortProviderExtension; } } diff --git a/src/HotChocolate/Data/src/Data/Sorting/Convention/SortConvention.cs b/src/HotChocolate/Data/src/Data/Sorting/Convention/SortConvention.cs index 33f49cb9eb6..280583aa825 100644 --- a/src/HotChocolate/Data/src/Data/Sorting/Convention/SortConvention.cs +++ b/src/HotChocolate/Data/src/Data/Sorting/Convention/SortConvention.cs @@ -51,6 +51,8 @@ public SortConvention(Action configure) throw new ArgumentNullException(nameof(configure)); } + internal new SortConventionDefinition? Definition => base.Definition; + protected override SortConventionDefinition CreateDefinition( IConventionContext context) { @@ -73,29 +75,27 @@ protected virtual void Configure(ISortConventionDescriptor descriptor) { } - protected override void OnComplete( - IConventionContext context, - SortConventionDefinition definition) + protected override void OnComplete(IConventionContext context) { - if (definition.Provider is null) + if (Definition?.Provider is null) { - throw SortConvention_NoProviderFound(GetType(), definition.Scope); + throw SortConvention_NoProviderFound(GetType(), Definition?.Scope); } - if (definition.ProviderInstance is null) + if (Definition.ProviderInstance is null) { _provider = - context.Services.GetOrCreateService(definition.Provider) ?? - throw SortConvention_NoProviderFound(GetType(), definition.Scope); + context.Services.GetOrCreateService(Definition.Provider) ?? + throw SortConvention_NoProviderFound(GetType(), Definition.Scope); } else { - _provider = definition.ProviderInstance; + _provider = Definition.ProviderInstance; } _namingConventions = context.DescriptorContext.Naming; - _operations = definition.Operations.ToDictionary( + _operations = Definition.Operations.ToDictionary( x => x.Id, SortOperation.FromDefinition); @@ -107,18 +107,26 @@ protected virtual void Configure(ISortConventionDescriptor descriptor) } } - _bindings = definition.Bindings; - _defaultBinding = definition.DefaultBinding; - _inputTypeConfigs = definition.Configurations; - _enumTypeConfigs = definition.EnumConfigurations; - _argumentName = definition.ArgumentName; + _bindings = Definition.Bindings; + _defaultBinding = Definition.DefaultBinding; + _inputTypeConfigs = Definition.Configurations; + _enumTypeConfigs = Definition.EnumConfigurations; + _argumentName = Definition.ArgumentName; if (_provider is ISortProviderConvention init) { + IReadOnlyList extensions = + CollectExtensions(context.Services, Definition); init.Initialize(context); + MergeExtensions(context, init, extensions); + init.OnComplete(context); } _typeInspector = context.DescriptorContext.TypeInspector; + + // It is important to always call base to continue the cleanup and the disposal of the + // definition + base.OnComplete(context); } @@ -295,5 +303,43 @@ public NameString GetOperationName(int operation) type = null; return false; } + + private static IReadOnlyList CollectExtensions( + IServiceProvider serviceProvider, + SortConventionDefinition definition) + { + List extensions = new List(); + extensions.AddRange(definition.ProviderExtensions); + foreach (var extensionType in definition.ProviderExtensionsTypes) + { + if (serviceProvider.TryGetOrCreateService( + extensionType, + out var createdExtension)) + { + extensions.Add(createdExtension); + } + } + + return extensions; + } + + private static void MergeExtensions( + IConventionContext context, + ISortProviderConvention provider, + IReadOnlyList extensions) + { + if (provider is Convention providerConvention) + { + for (var m = 0; m < extensions.Count; m++) + { + if (extensions[m] is ISortProviderConvention extensionConvention) + { + extensionConvention.Initialize(context); + extensions[m].Merge(context, providerConvention); + extensionConvention.OnComplete(context); + } + } + } + } } } diff --git a/src/HotChocolate/Data/src/Data/Sorting/Convention/SortConventionDefinition.cs b/src/HotChocolate/Data/src/Data/Sorting/Convention/SortConventionDefinition.cs index c317b994633..c430b90e0e5 100644 --- a/src/HotChocolate/Data/src/Data/Sorting/Convention/SortConventionDefinition.cs +++ b/src/HotChocolate/Data/src/Data/Sorting/Convention/SortConventionDefinition.cs @@ -7,9 +7,11 @@ namespace HotChocolate.Data.Sorting { public class SortConventionDefinition : IHasScope { + public static readonly string DefaultArgumentName = "order"; + public string? Scope { get; set; } - public string ArgumentName { get; set; } = "order"; + public string ArgumentName { get; set; } = DefaultArgumentName; public Type? Provider { get; set; } @@ -29,5 +31,10 @@ public class SortConventionDefinition : IHasScope public IDictionary> EnumConfigurations { get; } = new Dictionary>( TypeReferenceComparer.Default); + + public IList ProviderExtensions { get; } = + new List(); + + public IList ProviderExtensionsTypes { get; } = new List(); } } diff --git a/src/HotChocolate/Data/src/Data/Sorting/Convention/SortConventionDescriptor.cs b/src/HotChocolate/Data/src/Data/Sorting/Convention/SortConventionDescriptor.cs index 01e01795808..779244f3c04 100644 --- a/src/HotChocolate/Data/src/Data/Sorting/Convention/SortConventionDescriptor.cs +++ b/src/HotChocolate/Data/src/Data/Sorting/Convention/SortConventionDescriptor.cs @@ -192,6 +192,20 @@ public ISortConventionDescriptor ArgumentName(NameString argumentName) return this; } + public ISortConventionDescriptor AddProviderExtension() + where TExtension : class, ISortProviderExtension + { + Definition.ProviderExtensionsTypes.Add(typeof(TExtension)); + return this; + } + + public ISortConventionDescriptor AddProviderExtension(TExtension provider) + where TExtension : class, ISortProviderExtension + { + Definition.ProviderExtensions.Add(provider); + return this; + } + /// /// Creates a new descriptor for /// diff --git a/src/HotChocolate/Data/src/Data/Sorting/Convention/SortConventionExtension.cs b/src/HotChocolate/Data/src/Data/Sorting/Convention/SortConventionExtension.cs new file mode 100644 index 00000000000..e27647476c2 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Sorting/Convention/SortConventionExtension.cs @@ -0,0 +1,110 @@ +using System; +using HotChocolate.Data.Utilities; +using HotChocolate.Types.Descriptors; + +namespace HotChocolate.Data.Sorting +{ + /// + /// The sort convention extensions can be used to extend a sort convention. + /// + public class SortConventionExtension + : ConventionExtension + { + private Action? _configure; + + protected SortConventionExtension() + { + _configure = Configure; + } + + public SortConventionExtension(Action configure) + { + _configure = configure ?? + throw new ArgumentNullException(nameof(configure)); + } + + protected override SortConventionDefinition CreateDefinition( + IConventionContext context) + { + if (_configure is null) + { + throw new InvalidOperationException( + DataResources.SortConvention_NoConfigurationSpecified); + } + + var descriptor = SortConventionDescriptor.New( + context.DescriptorContext, + context.Scope); + + _configure(descriptor); + _configure = null; + + return descriptor.CreateDefinition(); + } + + protected internal new void Initialize(IConventionContext context) + { + base.Initialize(context); + } + + protected virtual void Configure(ISortConventionDescriptor descriptor) + { + } + + public override void Merge(IConventionContext context, Convention convention) + { + if (convention is SortConvention sortConvention && + Definition is not null && + sortConvention.Definition is not null) + { + ExtensionHelpers.MergeDictionary( + Definition.Bindings, + sortConvention.Definition.Bindings); + + ExtensionHelpers.MergeListDictionary( + Definition.Configurations, + sortConvention.Definition.Configurations); + + ExtensionHelpers.MergeListDictionary( + Definition.EnumConfigurations, + sortConvention.Definition.EnumConfigurations); + + for (var i = 0; i < Definition.Operations.Count; i++) + { + sortConvention.Definition.Operations.Add(Definition.Operations[i]); + } + + for (var i = 0; i < Definition.ProviderExtensions.Count; i++) + { + sortConvention.Definition.ProviderExtensions.Add(Definition.ProviderExtensions[i]); + } + + for (var i = 0; i < Definition.ProviderExtensionsTypes.Count; i++) + { + sortConvention.Definition.ProviderExtensionsTypes.Add( + Definition.ProviderExtensionsTypes[i]); + } + + if (Definition.ArgumentName != SortConventionDefinition.DefaultArgumentName) + { + sortConvention.Definition.ArgumentName = Definition.ArgumentName; + } + + if (Definition.Provider is not null) + { + sortConvention.Definition.Provider = Definition.Provider; + } + + if (Definition.ProviderInstance is not null) + { + sortConvention.Definition.ProviderInstance = Definition.ProviderInstance; + } + + if (Definition.DefaultBinding is not null) + { + sortConvention.Definition.DefaultBinding = Definition.DefaultBinding; + } + } + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Sorting/Expressions/Extensions/SortConventionDescriptorQueryableExtensions.cs b/src/HotChocolate/Data/src/Data/Sorting/Expressions/Extensions/SortConventionDescriptorQueryableExtensions.cs index c24b8df9971..38d2f702eb2 100644 --- a/src/HotChocolate/Data/src/Data/Sorting/Expressions/Extensions/SortConventionDescriptorQueryableExtensions.cs +++ b/src/HotChocolate/Data/src/Data/Sorting/Expressions/Extensions/SortConventionDescriptorQueryableExtensions.cs @@ -13,7 +13,7 @@ public static class SortConventionDescriptorQueryableExtensions { descriptor.AddOperationHandler(); descriptor.AddOperationHandler(); - descriptor.AddFieldHandler(); + descriptor.AddFieldHandler(); return descriptor; } } diff --git a/src/HotChocolate/Data/src/Data/Sorting/Expressions/Handlers/QueryableDefaultFieldHandler.cs b/src/HotChocolate/Data/src/Data/Sorting/Expressions/Handlers/QueryableDefaultSortFieldHandler.cs similarity index 98% rename from src/HotChocolate/Data/src/Data/Sorting/Expressions/Handlers/QueryableDefaultFieldHandler.cs rename to src/HotChocolate/Data/src/Data/Sorting/Expressions/Handlers/QueryableDefaultSortFieldHandler.cs index 09fc27d2dab..0cf1eea7366 100644 --- a/src/HotChocolate/Data/src/Data/Sorting/Expressions/Handlers/QueryableDefaultFieldHandler.cs +++ b/src/HotChocolate/Data/src/Data/Sorting/Expressions/Handlers/QueryableDefaultSortFieldHandler.cs @@ -8,7 +8,7 @@ namespace HotChocolate.Data.Sorting.Expressions { - public class QueryableDefaultFieldHandler + public class QueryableDefaultSortFieldHandler : SortFieldHandler { public override bool CanHandle( diff --git a/src/HotChocolate/Data/src/Data/Sorting/Expressions/QueryableSortProviderExtension.cs b/src/HotChocolate/Data/src/Data/Sorting/Expressions/QueryableSortProviderExtension.cs new file mode 100644 index 00000000000..246c799493d --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Sorting/Expressions/QueryableSortProviderExtension.cs @@ -0,0 +1,18 @@ +using System; + +namespace HotChocolate.Data.Sorting.Expressions +{ + public class QueryableSortProviderExtension + : SortProviderExtensions + { + public QueryableSortProviderExtension() + { + } + + public QueryableSortProviderExtension( + Action> configure) + : base(configure) + { + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Sorting/Visitor/ISortProviderConvention.cs b/src/HotChocolate/Data/src/Data/Sorting/Visitor/ISortProviderConvention.cs index 2d0165b071b..8c2d90df2b7 100644 --- a/src/HotChocolate/Data/src/Data/Sorting/Visitor/ISortProviderConvention.cs +++ b/src/HotChocolate/Data/src/Data/Sorting/Visitor/ISortProviderConvention.cs @@ -4,6 +4,8 @@ namespace HotChocolate.Data.Sorting { internal interface ISortProviderConvention { - void Initialize(IConventionContext context); + internal void Initialize(IConventionContext context); + + internal void OnComplete(IConventionContext context); } } diff --git a/src/HotChocolate/Data/src/Data/Sorting/Visitor/ISortProviderExtension.cs b/src/HotChocolate/Data/src/Data/Sorting/Visitor/ISortProviderExtension.cs new file mode 100644 index 00000000000..5097f4833fc --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Sorting/Visitor/ISortProviderExtension.cs @@ -0,0 +1,9 @@ +using HotChocolate.Types.Descriptors; + +namespace HotChocolate.Data.Sorting +{ + public interface ISortProviderExtension + : IConventionExtension + { + } +} diff --git a/src/HotChocolate/Data/src/Data/Sorting/Visitor/SortProvider.cs b/src/HotChocolate/Data/src/Data/Sorting/Visitor/SortProvider.cs index 662b8b8090c..5502ca85909 100644 --- a/src/HotChocolate/Data/src/Data/Sorting/Visitor/SortProvider.cs +++ b/src/HotChocolate/Data/src/Data/Sorting/Visitor/SortProvider.cs @@ -35,6 +35,8 @@ public SortProvider(Action> configure) throw new ArgumentNullException(nameof(configure)); } + internal new SortProviderDefinition? Definition => base.Definition; + public IReadOnlyCollection FieldHandlers => _fieldHandlers; public IReadOnlyCollection OperationHandlers => _operationHandlers; @@ -59,28 +61,36 @@ protected override SortProviderDefinition CreateDefinition(IConventionContext co return descriptor.CreateDefinition(); } - protected override void OnComplete( - IConventionContext context, - SortProviderDefinition definition) + void ISortProviderConvention.Initialize(IConventionContext context) + { + base.Initialize(context); + } + + void ISortProviderConvention.OnComplete(IConventionContext context) + { + OnComplete(context); + } + + protected override void OnComplete(IConventionContext context) { - if (definition.Handlers.Count == 0) + if (Definition?.Handlers.Count == 0) { throw SortProvider_NoFieldHandlersConfigured(this); } - if (definition.Handlers.Count == 0) + if (Definition.OperationHandlers.Count == 0) { throw SortProvider_NoOperationHandlersConfigured(this); } IServiceProvider services = new DictionaryServiceProvider( - (typeof(ISortProvider), this), - (typeof(IConventionContext), context), - (typeof(IDescriptorContext), context.DescriptorContext), - (typeof(ITypeInspector), context.DescriptorContext.TypeInspector)) + (typeof(ISortProvider), this), + (typeof(IConventionContext), context), + (typeof(IDescriptorContext), context.DescriptorContext), + (typeof(ITypeInspector), context.DescriptorContext.TypeInspector)) .Include(context.Services); - foreach ((Type Type, ISortFieldHandler? Instance) handler in definition.Handlers) + foreach ((Type Type, ISortFieldHandler? Instance) handler in Definition.Handlers) { switch (handler.Instance) { @@ -89,10 +99,11 @@ protected override SortProviderDefinition CreateDefinition(IConventionContext co out ISortFieldHandler? service): _fieldHandlers.Add(service); break; + case null: - context.ReportError( + throw new SchemaException( SortProvider_UnableToCreateFieldHandler(this, handler.Type)); - break; + case ISortFieldHandler casted: _fieldHandlers.Add(casted); break; @@ -100,7 +111,7 @@ protected override SortProviderDefinition CreateDefinition(IConventionContext co } foreach ((Type Type, ISortOperationHandler? Instance) handler - in definition.OperationHandlers) + in Definition.OperationHandlers) { switch (handler.Instance) { @@ -109,10 +120,11 @@ protected override SortProviderDefinition CreateDefinition(IConventionContext co out ISortOperationHandler? service): _operationHandlers.Add(service); break; + case null: - context.ReportError( + throw new SchemaException( SortProvider_UnableToCreateOperationHandler(this, handler.Type)); - break; + case ISortOperationHandler casted: _operationHandlers.Add(casted); break; diff --git a/src/HotChocolate/Data/src/Data/Sorting/Visitor/SortProviderDefiniton.cs b/src/HotChocolate/Data/src/Data/Sorting/Visitor/SortProviderDefiniton.cs index a40e0101e51..676a0c162b3 100644 --- a/src/HotChocolate/Data/src/Data/Sorting/Visitor/SortProviderDefiniton.cs +++ b/src/HotChocolate/Data/src/Data/Sorting/Visitor/SortProviderDefiniton.cs @@ -9,8 +9,6 @@ public class SortProviderDefinition new List<(Type Handler, ISortFieldHandler? HandlerInstance)>(); public IList<(Type Handler, ISortOperationHandler? HandlerInstance)> OperationHandlers - { - get; - } = new List<(Type Handler, ISortOperationHandler? HandlerInstance)>(); + { get; } = new List<(Type Handler, ISortOperationHandler? HandlerInstance)>(); } } diff --git a/src/HotChocolate/Data/src/Data/Sorting/Visitor/SortProviderExtensions.cs b/src/HotChocolate/Data/src/Data/Sorting/Visitor/SortProviderExtensions.cs new file mode 100644 index 00000000000..9eecef0ca7b --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Sorting/Visitor/SortProviderExtensions.cs @@ -0,0 +1,74 @@ +using System; +using HotChocolate.Types.Descriptors; +using static HotChocolate.Data.DataResources; + +namespace HotChocolate.Data.Sorting +{ + public abstract class SortProviderExtensions + : ConventionExtension, + ISortProviderExtension, + ISortProviderConvention + where TContext : ISortVisitorContext + { + private Action>? _configure; + + protected SortProviderExtensions() + { + _configure = Configure; + } + + public SortProviderExtensions(Action> configure) + { + _configure = configure ?? + throw new ArgumentNullException(nameof(configure)); + } + + void ISortProviderConvention.Initialize(IConventionContext context) + { + base.Initialize(context); + } + + void ISortProviderConvention.OnComplete(IConventionContext context) + { + OnComplete(context); + } + + protected override SortProviderDefinition CreateDefinition(IConventionContext context) + { + if (_configure is null) + { + throw new InvalidOperationException(SortProvider_NoConfigurationSpecified); + } + + var descriptor = SortProviderDescriptor.New(); + + _configure(descriptor); + _configure = null; + + return descriptor.CreateDefinition(); + } + + protected virtual void Configure(ISortProviderDescriptor descriptor) { } + + public override void Merge(IConventionContext context, Convention convention) + { + if (Definition is {} && + convention is SortProvider conv && + conv.Definition is {} target) + { + // Provider extensions should be applied by default before the default handlers, as + // the interceptor picks up the first handler. A provider extension should adds more + // specific handlers than the default providers + for (var i = Definition.Handlers.Count - 1; i >= 0 ; i--) + { + target.Handlers.Insert(0, Definition.Handlers[i]); + } + + for (var i = Definition.OperationHandlers.Count - 1; i >= 0 ; i--) + { + target.OperationHandlers.Insert(0, Definition.OperationHandlers[i]); + } + } + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Utilities/ExtensionHelpers.cs b/src/HotChocolate/Data/src/Data/Utilities/ExtensionHelpers.cs new file mode 100644 index 00000000000..e5c40270b95 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Utilities/ExtensionHelpers.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; + +namespace HotChocolate.Data.Utilities +{ + internal static class ExtensionHelpers + { + public static void MergeListDictionary( + IDictionary> from, + IDictionary> to) + where TKey : notnull + { + foreach (KeyValuePair> element in from) + { + if (to.TryGetValue(element.Key, out var configurations)) + { + configurations.AddRange(element.Value); + } + else + { + to[element.Key] = element.Value; + } + } + } + + public static void MergeDictionary( + IDictionary from, + IDictionary to) + where TKey : notnull + { + foreach (KeyValuePair element in from) + { + to[element.Key] = element.Value; + } + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Filters.Tests/Convention/FilterConventionTests.cs b/src/HotChocolate/Data/test/Data.Filters.Tests/Convention/FilterConventionTests.cs index 7f6803605f6..94f6ae8e7db 100644 --- a/src/HotChocolate/Data/test/Data.Filters.Tests/Convention/FilterConventionTests.cs +++ b/src/HotChocolate/Data/test/Data.Filters.Tests/Convention/FilterConventionTests.cs @@ -40,10 +40,10 @@ public void FilterConvention_Should_Work_When_ConfigurationIsComplete() Func? func = executor.Build(value); // assert - var a = new Foo {Bar = "a"}; + var a = new Foo { Bar = "a" }; Assert.True(func(a)); - var b = new Foo {Bar = "b"}; + var b = new Foo { Bar = "b" }; Assert.False(func(b)); } @@ -223,7 +223,135 @@ public void FilterConvention_Should_Fail_When_NoMatchingBindingWasFound() #endif } - protected ISchema CreateSchemaWith(IFilterInputType type, FilterConvention convention) + [Fact] + public void FilterConvention_Should_Work_With_Extensions() + { + // arrange + var provider = new QueryableFilterProvider( + descriptor => + { + descriptor.AddFieldHandler(); + descriptor.AddFieldHandler(); + }); + + var convention = new FilterConvention( + descriptor => + { + }); + + var extension1 = new FilterConventionExtension( + descriptor => + { + descriptor.BindRuntimeType(); + descriptor.Provider(provider); + }); + + var extension2 = new FilterConventionExtension( + descriptor => + { + descriptor.Operation(DefaultOperations.Equals).Name("eq"); + }); + + IValueNode value = Utf8GraphQLParser.Syntax.ParseValueLiteral("{ bar: { eq:\"a\" }}"); + var type = new FooFilterType(); + + //act + CreateSchemaWith(type, convention, extension1, extension2); + var executor = new ExecutorBuilder(type); + + Func func = executor.Build(value); + + // assert + var a = new Foo { Bar = "a" }; + Assert.True(func(a)); + + var b = new Foo { Bar = "b" }; + Assert.False(func(b)); + } + + [Fact] + public void FilterConvention_Should_Work_With_ExtensionsType() + { + // arrange + var provider = new QueryableFilterProvider( + descriptor => + { + descriptor.AddFieldHandler(); + descriptor.AddFieldHandler(); + }); + + var convention = new FilterConvention( + descriptor => + { + descriptor.BindRuntimeType(); + descriptor.Provider(provider); + }); + + IValueNode value = Utf8GraphQLParser.Syntax.ParseValueLiteral("{ bar: { eq:\"a\" }}"); + var type = new FooFilterType(); + + //act + CreateSchemaWithTypes( + type, + convention, + typeof(MockFilterExtensionConvention)); + var executor = new ExecutorBuilder(type); + + Func func = executor.Build(value); + + // assert + var a = new Foo { Bar = "a" }; + Assert.True(func(a)); + + var b = new Foo { Bar = "b" }; + Assert.False(func(b)); + } + + [Fact] + public void FilterConvention_Should_Work_With_ProviderExtensionsType() + { + // arrange + var provider = new QueryableFilterProvider( + descriptor => + { + descriptor.AddFieldHandler(); + }); + + var convention = new FilterConvention( + descriptor => + { + descriptor.BindRuntimeType(); + descriptor.Provider(provider); + }); + + var extension1 = new FilterConventionExtension( + descriptor => + { + descriptor.Operation(DefaultOperations.Equals).Name("eq"); + descriptor.AddProviderExtension(); + }); + + IValueNode value = Utf8GraphQLParser.Syntax.ParseValueLiteral("{ bar: { eq:\"a\" }}"); + var type = new FooFilterType(); + + //act + CreateSchemaWith(type, convention, extension1); + var executor = new ExecutorBuilder(type); + + Func func = executor.Build(value); + + // assert + var a = new Foo { Bar = "a" }; + Assert.True(func(a)); + + var b = new Foo { Bar = "b" }; + Assert.False(func(b)); + } + + protected ISchema CreateSchemaWithTypes( + IFilterInputType type, + FilterConvention convention, + params Type[] extensions) { ISchemaBuilder builder = SchemaBuilder.New() .AddConvention(convention) @@ -236,9 +364,55 @@ protected ISchema CreateSchemaWith(IFilterInputType type, FilterConvention conve .Resolver("bar")) .AddType(type); + foreach (var extension in extensions) + { + builder.AddConvention(extension); + } + return builder.Create(); } + protected ISchema CreateSchemaWith( + IFilterInputType type, + FilterConvention convention, + params FilterConventionExtension[] extensions) + { + ISchemaBuilder builder = SchemaBuilder.New() + .AddConvention(convention) + .AddFiltering() + .AddQueryType( + c => + c.Name("Query") + .Field("foo") + .Type() + .Resolver("bar")) + .AddType(type); + + foreach (var extension in extensions) + { + builder.AddConvention(extension); + } + + return builder.Create(); + } + + public class MockFilterProviderExtensionConvention : QueryableFilterProviderExtensions + { + protected override void Configure( + IFilterProviderDescriptor descriptor) + { + descriptor.AddFieldHandler(); + } + } + + public class MockFilterExtensionConvention : FilterConventionExtension + { + protected override void Configure(IFilterConventionDescriptor descriptor) + { + descriptor.Operation(DefaultOperations.Equals).Name("eq"); + } + } + public class TestOperationFilterType : StringOperationFilterInput { protected override void Configure(IFilterInputTypeDescriptor descriptor) diff --git a/src/HotChocolate/Data/test/Data.Filters.Tests/FilterConventionExtensionsTests.cs b/src/HotChocolate/Data/test/Data.Filters.Tests/FilterConventionExtensionsTests.cs new file mode 100644 index 00000000000..6d8aa196f68 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Filters.Tests/FilterConventionExtensionsTests.cs @@ -0,0 +1,285 @@ +using System; +using System.Collections.Generic; +using HotChocolate.Data.Filters; +using HotChocolate.Data.Filters.Expressions; +using HotChocolate.Resolvers; +using HotChocolate.Types; +using HotChocolate.Types.Descriptors; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace HotChocolate.Data +{ + public class FilterConventionExtensionsTests + { + [Fact] + public void Merge_Should_Merge_ArgumentName() + { + // arrange + var convention = new MockFilterConvention(x => x.ArgumentName("Foo")); + var extension = new FilterConventionExtension(x => x.ArgumentName("Bar")); + var context = new ConventionContext( + "Scope", + new ServiceCollection().BuildServiceProvider(), + DescriptorContext.Create()); + + convention.Initialize(context); + extension.Initialize(context); + + // act + extension.Merge(context, convention); + + // assert + Assert.Equal("Bar", convention.DefinitionAccessor?.ArgumentName); + } + + [Fact] + public void Merge_Should_NotMerge_ArgumentName_When_Default() + { + // arrange + var convention = new MockFilterConvention(x => x.ArgumentName("Foo")); + var extension = new FilterConventionExtension( + x => x.ArgumentName(FilterConventionDefinition.DefaultArgumentName)); + var context = new ConventionContext( + "Scope", + new ServiceCollection().BuildServiceProvider(), + DescriptorContext.Create()); + + convention.Initialize(context); + extension.Initialize(context); + + // act + extension.Merge(context, convention); + + // assert + Assert.Equal("Foo", convention.DefinitionAccessor?.ArgumentName); + } + + [Fact] + public void Merge_Should_Merge_Provider() + { + // arrange + var convention = new MockFilterConvention(x => x.Provider()); + var extension = new FilterConventionExtension(x => x.Provider()); + var context = new ConventionContext( + "Scope", + new ServiceCollection().BuildServiceProvider(), + DescriptorContext.Create()); + + convention.Initialize(context); + extension.Initialize(context); + + // act + extension.Merge(context, convention); + + // assert + Assert.Equal(typeof(MockProvider), convention.DefinitionAccessor?.Provider); + } + + [Fact] + public void Merge_Should_Merge_ProviderInstance() + { + // arrange + var providerInstance = new MockProvider(); + var convention = new MockFilterConvention( + x => x.Provider(new QueryableFilterProvider())); + var extension = new FilterConventionExtension( + x => x.Provider(providerInstance)); + var context = new ConventionContext( + "Scope", + new ServiceCollection().BuildServiceProvider(), + DescriptorContext.Create()); + + convention.Initialize(context); + extension.Initialize(context); + + // act + extension.Merge(context, convention); + + // assert + Assert.Equal(providerInstance, convention.DefinitionAccessor?.ProviderInstance); + } + + [Fact] + public void Merge_Should_Merge_Operations() + { + // arrange + var convention = new MockFilterConvention(x => x.Operation(1)); + var extension = new FilterConventionExtension(x => x.Operation(2)); + var context = new ConventionContext( + "Scope", + new ServiceCollection().BuildServiceProvider(), + DescriptorContext.Create()); + + convention.Initialize(context); + extension.Initialize(context); + + // act + extension.Merge(context, convention); + + // assert + Assert.NotNull(convention.DefinitionAccessor); + Assert.Collection( + convention.DefinitionAccessor!.Operations, + x => Assert.Equal(1, x.Id), + x => Assert.Equal(2, x.Id)); + } + + [Fact] + public void Merge_Should_Merge_Bindings() + { + // arrange + var convention = new MockFilterConvention( + x => x.BindRuntimeType>()); + var extension = new FilterConventionExtension( + x => x.BindRuntimeType>()); + var context = new ConventionContext( + "Scope", + new ServiceCollection().BuildServiceProvider(), + DescriptorContext.Create()); + + convention.Initialize(context); + extension.Initialize(context); + + // act + extension.Merge(context, convention); + + // assert + Assert.NotNull(convention.DefinitionAccessor); + Assert.Contains(typeof(int), convention.DefinitionAccessor!.Bindings); + Assert.Contains(typeof(double), convention.DefinitionAccessor!.Bindings); + } + + [Fact] + public void Merge_Should_DeepMerge_Configurations() + { + // arrange + var convention = new MockFilterConvention( + x => x.Configure>(d => d.Name("Foo"))); + var extension = new FilterConventionExtension( + x => x.Configure>(d => d.Name("Bar"))); + var context = new ConventionContext( + "Scope", + new ServiceCollection().BuildServiceProvider(), + DescriptorContext.Create()); + + convention.Initialize(context); + extension.Initialize(context); + + // act + extension.Merge(context, convention); + + // assert + Assert.NotNull(convention.DefinitionAccessor); + List configuration = + Assert.Single(convention.DefinitionAccessor!.Configurations.Values)!; + Assert.Equal(2, configuration.Count); + } + + [Fact] + public void Merge_Should_Merge_Configurations() + { + // arrange + var convention = new MockFilterConvention( + x => x.Configure>(d => d.Name("Foo"))); + var extension = new FilterConventionExtension( + x => x.Configure>(d => d.Name("Bar"))); + var context = new ConventionContext( + "Scope", + new ServiceCollection().BuildServiceProvider(), + DescriptorContext.Create()); + + convention.Initialize(context); + extension.Initialize(context); + + // act + extension.Merge(context, convention); + + // assert + Assert.NotNull(convention.DefinitionAccessor); + Assert.Equal(2, convention.DefinitionAccessor!.Configurations.Count); + } + + [Fact] + public void Merge_Should_Merge_ProviderExtensionsTypes() + { + // arrange + var convention = + new MockFilterConvention(x => x.AddProviderExtension()); + var extension = + new FilterConventionExtension( + x => x.AddProviderExtension()); + var context = new ConventionContext( + "Scope", + new ServiceCollection().BuildServiceProvider(), + DescriptorContext.Create()); + + convention.Initialize(context); + extension.Initialize(context); + + // act + extension.Merge(context, convention); + + // assert + Assert.NotNull(convention.DefinitionAccessor); + Assert.Equal(2, convention.DefinitionAccessor!.ProviderExtensionsTypes.Count); + } + + [Fact] + public void Merge_Should_Merge_ProviderExtensions() + { + // arrange + var provider1 = new MockProviderExtensions(); + var convention = new MockFilterConvention(x => x.AddProviderExtension(provider1)); + var provider2 = new MockProviderExtensions(); + var extension = new FilterConventionExtension(x => x.AddProviderExtension(provider2)); + var context = new ConventionContext( + "Scope", + new ServiceCollection().BuildServiceProvider(), + DescriptorContext.Create()); + + convention.Initialize(context); + extension.Initialize(context); + + // act + extension.Merge(context, convention); + + // assert + Assert.NotNull(convention.DefinitionAccessor); + Assert.Collection( + convention.DefinitionAccessor!.ProviderExtensions, + x => Assert.Equal(provider1, x), + x => Assert.Equal(provider2, x)); + } + + private class MockProviderExtensions : FilterProviderExtensions + { + } + + private class MockProvider : IFilterProvider + { + public IReadOnlyCollection FieldHandlers { get; } = null!; + + public FieldMiddleware CreateExecutor(NameString argumentName) + { + throw new NotImplementedException(); + } + + public void ConfigureField(NameString argumentName, IObjectFieldDescriptor descriptor) + { + throw new NotImplementedException(); + } + } + + private class MockFilterConvention : FilterConvention + { + public MockFilterConvention( + Action configure) + : base(configure) + { + } + + public FilterConventionDefinition? DefinitionAccessor => base.Definition; + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Filters.Tests/FilterProviderExtensionsTests.cs b/src/HotChocolate/Data/test/Data.Filters.Tests/FilterProviderExtensionsTests.cs new file mode 100644 index 00000000000..871f746b0d9 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Filters.Tests/FilterProviderExtensionsTests.cs @@ -0,0 +1,66 @@ +using System; +using HotChocolate.Data.Filters; +using HotChocolate.Data.Filters.Expressions; +using HotChocolate.Resolvers; +using HotChocolate.Types.Descriptors; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace HotChocolate.Data +{ + public class FilterProviderExtensionsTests + { + [Fact] + public void Merge_Should_Merge_HandlersAndPrependExtensionHandlers() + { + // arrange + var firstFieldHandler = new QueryableStringContainsHandler(); + var extensionFieldHandler = new QueryableStringContainsHandler(); + var convention = new MockProvider(x => x.AddFieldHandler(firstFieldHandler)); + var extension = new MockProviderExtensions( + x => x.AddFieldHandler(extensionFieldHandler)); + var context = new ConventionContext( + "Scope", + new ServiceCollection().BuildServiceProvider(), + DescriptorContext.Create()); + + convention.Initialize(context); + extension.Initialize(context); + + // act + extension.Merge(context, convention); + + // assert + Assert.NotNull(convention.DefinitionAccessor); + Assert.Collection( + convention.DefinitionAccessor!.Handlers, + x => Assert.Equal(extensionFieldHandler, x.HandlerInstance), + x => Assert.Equal(firstFieldHandler, x.HandlerInstance)); + } + + private class MockProviderExtensions + : FilterProviderExtensions + { + public MockProviderExtensions( + Action> configure) + : base(configure) + { + } + } + + private class MockProvider : FilterProvider + { + public FilterProviderDefinition? DefinitionAccessor => base.Definition; + + public MockProvider(Action> configure) + : base(configure) + { + } + + public override FieldMiddleware CreateExecutor(NameString argumentName) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Filters.Tests/FilterTypeAttributeTests.cs b/src/HotChocolate/Data/test/Data.Filters.Tests/FilterTypeAttributeTests.cs index 80b871dc4db..dc5fcf6a3b2 100644 --- a/src/HotChocolate/Data/test/Data.Filters.Tests/FilterTypeAttributeTests.cs +++ b/src/HotChocolate/Data/test/Data.Filters.Tests/FilterTypeAttributeTests.cs @@ -59,7 +59,7 @@ public class GenericTypeFilterAttribute { public static string TypeName { get; } = "ThisIsATest"; - protected override void TryConfigure( + protected internal override void TryConfigure( IDescriptorContext context, IDescriptor d, ICustomAttributeProvider element) @@ -76,7 +76,7 @@ public class FilterFieldAttributeTest { public static string Field { get; } = "FieldField"; - protected override void TryConfigure( + protected internal override void TryConfigure( IDescriptorContext context, IDescriptor descriptor, ICustomAttributeProvider element) diff --git a/src/HotChocolate/Data/test/Data.Sorting.Tests/Convention/SortConventionTests.cs b/src/HotChocolate/Data/test/Data.Sorting.Tests/Convention/SortConventionTests.cs index fd90792ff2b..e427af09ba6 100644 --- a/src/HotChocolate/Data/test/Data.Sorting.Tests/Convention/SortConventionTests.cs +++ b/src/HotChocolate/Data/test/Data.Sorting.Tests/Convention/SortConventionTests.cs @@ -17,7 +17,7 @@ public void SortConvention_Should_Work_When_ConfigurationIsComplete() descriptor => { descriptor.AddOperationHandler(); - descriptor.AddFieldHandler(); + descriptor.AddFieldHandler(); }); var convention = new SortConvention( @@ -28,24 +28,24 @@ public void SortConvention_Should_Work_When_ConfigurationIsComplete() descriptor.Provider(provider); }); - IValueNode? value = Utf8GraphQLParser.Syntax.ParseValueLiteral("{ bar: ASC}"); + IValueNode value = Utf8GraphQLParser.Syntax.ParseValueLiteral("{ bar: ASC}"); var type = new FooSortType(); //act - ISchema schema = CreateSchemaWith(type, convention); + CreateSchemaWith(type, convention); var executor = new ExecutorBuilder(type); Func func = executor.Build(value); // assert - var a = new[] {new Foo {Bar = "a"}, new Foo {Bar = "b"}, new Foo {Bar = "c"}}; + var a = new[] { new Foo { Bar = "a" }, new Foo { Bar = "b" }, new Foo { Bar = "c" } }; Assert.Collection( func(a), x => Assert.Equal("a", x.Bar), x => Assert.Equal("b", x.Bar), x => Assert.Equal("c", x.Bar)); - var b = new[] {new Foo {Bar = "c"}, new Foo {Bar = "b"}, new Foo {Bar = "a"}}; + var b = new[] { new Foo { Bar = "c" }, new Foo { Bar = "b" }, new Foo { Bar = "a" } }; Assert.Collection( func(b), x => Assert.Equal("a", x.Bar), @@ -60,7 +60,7 @@ public void SortConvention_Should_Fail_When_OperationHandlerIsNotRegistered() var provider = new QueryableSortProvider( descriptor => { - descriptor.AddFieldHandler(); + descriptor.AddFieldHandler(); }); var convention = new SortConvention( @@ -74,7 +74,7 @@ public void SortConvention_Should_Fail_When_OperationHandlerIsNotRegistered() var type = new FooSortType(); //act - SchemaException? error = + SchemaException error = Assert.Throws(() => CreateSchemaWith(type, convention)); Assert.Single(error.Errors); @@ -117,7 +117,7 @@ public void SortConvention_Should_Fail_When_OperationsInUknown() descriptor => { descriptor.AddOperationHandler(); - descriptor.AddFieldHandler(); + descriptor.AddFieldHandler(); }); var convention = new SortConvention( @@ -145,7 +145,7 @@ public void SortConvention_Should_Fail_When_OperationsIsNotNamed() descriptor => { descriptor.AddOperationHandler(); - descriptor.AddFieldHandler(); + descriptor.AddFieldHandler(); }); var convention = new SortConvention( @@ -173,7 +173,7 @@ public void SortConvention_Should_Fail_When_NoProviderWasRegistered() descriptor => { descriptor.AddOperationHandler(); - descriptor.AddFieldHandler(); + descriptor.AddFieldHandler(); }); var convention = new SortConvention( @@ -201,7 +201,7 @@ public void SortConvention_Should_Fail_When_NoMatchingBindingWasFound() descriptor => { descriptor.AddOperationHandler(); - descriptor.AddFieldHandler(); + descriptor.AddFieldHandler(); }); var convention = new SortConvention( @@ -221,7 +221,159 @@ public void SortConvention_Should_Fail_When_NoMatchingBindingWasFound() error.Errors[0].Message.MatchSnapshot(); } - protected ISchema CreateSchemaWith(ISortInputType type, SortConvention convention) + [Fact] + public void SortConvention_Should_Work_With_Extensions() + { + // arrange + var provider = new QueryableSortProvider( + descriptor => + { + descriptor.AddOperationHandler(); + descriptor.AddFieldHandler(); + }); + + var convention = new SortConvention( + descriptor => + { + }); + + var extension1 = new SortConventionExtension( + descriptor => + { + descriptor.Operation(DefaultSortOperations.Ascending).Name("ASC"); + descriptor.Provider(provider); + }); + + var extension2 = new SortConventionExtension( + descriptor => + { + descriptor.BindRuntimeType(); + }); + + IValueNode value = Utf8GraphQLParser.Syntax.ParseValueLiteral("{ bar: ASC}"); + var type = new FooSortType(); + + //act + CreateSchemaWith(type, convention, extension1, extension2); + var executor = new ExecutorBuilder(type); + + Func func = executor.Build(value); + + // assert + var a = new[] { new Foo { Bar = "a" }, new Foo { Bar = "b" }, new Foo { Bar = "c" } }; + Assert.Collection( + func(a), + x => Assert.Equal("a", x.Bar), + x => Assert.Equal("b", x.Bar), + x => Assert.Equal("c", x.Bar)); + + var b = new[] { new Foo { Bar = "c" }, new Foo { Bar = "b" }, new Foo { Bar = "a" } }; + Assert.Collection( + func(b), + x => Assert.Equal("a", x.Bar), + x => Assert.Equal("b", x.Bar), + x => Assert.Equal("c", x.Bar)); + } + + [Fact] + public void SortConvention_Should_Work_With_ExtensionsType() + { + // arrange + var provider = new QueryableSortProvider( + descriptor => + { + descriptor.AddOperationHandler(); + descriptor.AddFieldHandler(); + }); + + var convention = new SortConvention( + descriptor => + { + descriptor.BindRuntimeType(); + descriptor.Provider(provider); + }); + + IValueNode value = Utf8GraphQLParser.Syntax.ParseValueLiteral("{ bar: ASC}"); + var type = new FooSortType(); + + //act + CreateSchemaWithTypes( + type, + convention, + typeof(MockSortExtensionConvention)); + var executor = new ExecutorBuilder(type); + + Func func = executor.Build(value); + + // assert + var a = new[] { new Foo { Bar = "a" }, new Foo { Bar = "b" }, new Foo { Bar = "c" } }; + Assert.Collection( + func(a), + x => Assert.Equal("a", x.Bar), + x => Assert.Equal("b", x.Bar), + x => Assert.Equal("c", x.Bar)); + + var b = new[] { new Foo { Bar = "c" }, new Foo { Bar = "b" }, new Foo { Bar = "a" } }; + Assert.Collection( + func(b), + x => Assert.Equal("a", x.Bar), + x => Assert.Equal("b", x.Bar), + x => Assert.Equal("c", x.Bar)); + } + + [Fact] + public void SortConvention_Should_Work_With_ProviderExtensionsType() + { + // arrange + var provider = new QueryableSortProvider( + descriptor => + { + descriptor.AddFieldHandler(); + }); + + var convention = new SortConvention( + descriptor => + { + descriptor.BindRuntimeType(); + descriptor.Provider(provider); + }); + + var extension1 = new SortConventionExtension( + descriptor => + { + descriptor.Operation(DefaultSortOperations.Ascending).Name("ASC"); + descriptor.AddProviderExtension(); + }); + + IValueNode value = Utf8GraphQLParser.Syntax.ParseValueLiteral("{ bar: ASC}"); + var type = new FooSortType(); + + //act + CreateSchemaWith(type, convention, extension1); + var executor = new ExecutorBuilder(type); + + Func func = executor.Build(value); + + // assert + var a = new[] { new Foo { Bar = "a" }, new Foo { Bar = "b" }, new Foo { Bar = "c" } }; + Assert.Collection( + func(a), + x => Assert.Equal("a", x.Bar), + x => Assert.Equal("b", x.Bar), + x => Assert.Equal("c", x.Bar)); + + var b = new[] { new Foo { Bar = "c" }, new Foo { Bar = "b" }, new Foo { Bar = "a" } }; + Assert.Collection( + func(b), + x => Assert.Equal("a", x.Bar), + x => Assert.Equal("b", x.Bar), + x => Assert.Equal("c", x.Bar)); + } + + protected ISchema CreateSchemaWithTypes( + ISortInputType type, + SortConvention convention, + params Type[] extensions) { ISchemaBuilder builder = SchemaBuilder.New() .AddConvention(convention) @@ -234,9 +386,55 @@ protected ISchema CreateSchemaWith(ISortInputType type, SortConvention conventio .Resolver("bar")) .AddType(type); + foreach (var extension in extensions) + { + builder.AddConvention(extension); + } + return builder.Create(); } + protected ISchema CreateSchemaWith( + ISortInputType type, + SortConvention convention, + params SortConventionExtension[] extensions) + { + ISchemaBuilder builder = SchemaBuilder.New() + .AddConvention(convention) + .AddSorting() + .AddQueryType( + c => + c.Name("Query") + .Field("foo") + .Type() + .Resolver("bar")) + .AddType(type); + + foreach (var extension in extensions) + { + builder.AddConvention(extension); + } + + return builder.Create(); + } + + public class MockSortProviderExtensionConvention : QueryableSortProviderExtension + { + protected override void Configure( + ISortProviderDescriptor descriptor) + { + descriptor.AddOperationHandler(); + } + } + + public class MockSortExtensionConvention : SortConventionExtension + { + protected override void Configure(ISortConventionDescriptor descriptor) + { + descriptor.Operation(DefaultSortOperations.Ascending).Name("ASC"); + } + } + public class TestEnumType : SortEnumType { protected override void Configure(ISortEnumTypeDescriptor descriptor) diff --git a/src/HotChocolate/Data/test/Data.Sorting.Tests/Convention/__snapshots__/SortConventionTests.SortConvention_Should_Fail_When_OperationHandlerIsNotRegistered.snap b/src/HotChocolate/Data/test/Data.Sorting.Tests/Convention/__snapshots__/SortConventionTests.SortConvention_Should_Fail_When_OperationHandlerIsNotRegistered.snap index b569c9f6909..5d9e87867b0 100644 --- a/src/HotChocolate/Data/test/Data.Sorting.Tests/Convention/__snapshots__/SortConventionTests.SortConvention_Should_Fail_When_OperationHandlerIsNotRegistered.snap +++ b/src/HotChocolate/Data/test/Data.Sorting.Tests/Convention/__snapshots__/SortConventionTests.SortConvention_Should_Fail_When_OperationHandlerIsNotRegistered.snap @@ -1,3 +1,3 @@ For more details look at the `Errors` property. -1. For the value asc of type TestEnumType was no operation handler found. +1. The sort provider `HotChocolate.Data.Sorting.Expressions.QueryableSortProvider` does not specify and operation handler. diff --git a/src/HotChocolate/Data/test/Data.Sorting.Tests/SortConventionExtensionsTests.cs b/src/HotChocolate/Data/test/Data.Sorting.Tests/SortConventionExtensionsTests.cs new file mode 100644 index 00000000000..bf96dbb9719 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Sorting.Tests/SortConventionExtensionsTests.cs @@ -0,0 +1,375 @@ +using System; +using System.Collections.Generic; +using HotChocolate.Data.Sorting; +using HotChocolate.Data.Sorting.Expressions; +using HotChocolate.Resolvers; +using HotChocolate.Types; +using HotChocolate.Types.Descriptors; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace HotChocolate.Data +{ + public class SortConventionExtensionsTests + { + [Fact] + public void Merge_Should_Merge_ArgumentName() + { + // arrange + var convention = new MockSortConvention(x => x.ArgumentName("Foo")); + var extension = new SortConventionExtension(x => x.ArgumentName("Bar")); + var context = new ConventionContext( + "Scope", + new ServiceCollection().BuildServiceProvider(), + DescriptorContext.Create()); + + convention.Initialize(context); + extension.Initialize(context); + + // act + extension.Merge(context, convention); + + // assert + Assert.Equal("Bar", convention.DefinitionAccessor?.ArgumentName); + } + + [Fact] + public void Merge_Should_NotMerge_ArgumentName_When_Default() + { + // arrange + var convention = new MockSortConvention(x => x.ArgumentName("Foo")); + var extension = new SortConventionExtension( + x => x.ArgumentName(SortConventionDefinition.DefaultArgumentName)); + var context = new ConventionContext( + "Scope", + new ServiceCollection().BuildServiceProvider(), + DescriptorContext.Create()); + + convention.Initialize(context); + extension.Initialize(context); + + // act + extension.Merge(context, convention); + + // assert + Assert.Equal("Foo", convention.DefinitionAccessor?.ArgumentName); + } + + [Fact] + public void Merge_Should_Merge_Provider() + { + // arrange + var convention = new MockSortConvention(x => x.Provider()); + var extension = new SortConventionExtension(x => x.Provider()); + var context = new ConventionContext( + "Scope", + new ServiceCollection().BuildServiceProvider(), + DescriptorContext.Create()); + + convention.Initialize(context); + extension.Initialize(context); + + // act + extension.Merge(context, convention); + + // assert + Assert.Equal(typeof(MockProvider), convention.DefinitionAccessor?.Provider); + } + + [Fact] + public void Merge_Should_Merge_ProviderInstance() + { + // arrange + var providerInstance = new MockProvider(); + var convention = new MockSortConvention( + x => x.Provider(new QueryableSortProvider())); + var extension = new SortConventionExtension( + x => x.Provider(providerInstance)); + var context = new ConventionContext( + "Scope", + new ServiceCollection().BuildServiceProvider(), + DescriptorContext.Create()); + + convention.Initialize(context); + extension.Initialize(context); + + // act + extension.Merge(context, convention); + + // assert + Assert.Equal(providerInstance, convention.DefinitionAccessor?.ProviderInstance); + } + + [Fact] + public void Merge_Should_Merge_DefaultBinding() + { + // arrange + var convention = new MockSortConvention( + x => x.DefaultBinding()); + var extension = new SortConventionExtension( + x => x.DefaultBinding()); + var context = new ConventionContext( + "Scope", + new ServiceCollection().BuildServiceProvider(), + DescriptorContext.Create()); + + convention.Initialize(context); + extension.Initialize(context); + + // act + extension.Merge(context, convention); + + // assert + Assert.Equal( + typeof(MockSortEnumType), + convention.DefinitionAccessor?.DefaultBinding); + } + + [Fact] + public void Merge_Should_Merge_Operations() + { + // arrange + var convention = new MockSortConvention(x => x.Operation(1)); + var extension = new SortConventionExtension(x => x.Operation(2)); + var context = new ConventionContext( + "Scope", + new ServiceCollection().BuildServiceProvider(), + DescriptorContext.Create()); + + convention.Initialize(context); + extension.Initialize(context); + + // act + extension.Merge(context, convention); + + // assert + Assert.NotNull(convention.DefinitionAccessor); + Assert.Collection( + convention.DefinitionAccessor!.Operations, + x => Assert.Equal(1, x.Id), + x => Assert.Equal(2, x.Id)); + } + + [Fact] + public void Merge_Should_Merge_Bindings() + { + // arrange + var convention = new MockSortConvention( + x => x.BindRuntimeType()); + var extension = new SortConventionExtension( + x => x.BindRuntimeType()); + var context = new ConventionContext( + "Scope", + new ServiceCollection().BuildServiceProvider(), + DescriptorContext.Create()); + + convention.Initialize(context); + extension.Initialize(context); + + // act + extension.Merge(context, convention); + + // assert + Assert.NotNull(convention.DefinitionAccessor); + Assert.Contains(typeof(int), convention.DefinitionAccessor!.Bindings); + Assert.Contains(typeof(double), convention.DefinitionAccessor!.Bindings); + } + + [Fact] + public void Merge_Should_DeepMerge_Configurations() + { + // arrange + var convention = new MockSortConvention( + x => x.Configure>(d => d.Name("Foo"))); + var extension = new SortConventionExtension( + x => x.Configure>(d => d.Name("Bar"))); + var context = new ConventionContext( + "Scope", + new ServiceCollection().BuildServiceProvider(), + DescriptorContext.Create()); + + convention.Initialize(context); + extension.Initialize(context); + + // act + extension.Merge(context, convention); + + // assert + Assert.NotNull(convention.DefinitionAccessor); + List configuration = + Assert.Single(convention.DefinitionAccessor!.Configurations.Values)!; + Assert.Equal(2, configuration.Count); + } + + [Fact] + public void Merge_Should_Merge_Configurations() + { + // arrange + var convention = new MockSortConvention( + x => x.Configure>(d => d.Name("Foo"))); + var extension = new SortConventionExtension( + x => x.Configure>(d => d.Name("Foo"))); + var context = new ConventionContext( + "Scope", + new ServiceCollection().BuildServiceProvider(), + DescriptorContext.Create()); + + convention.Initialize(context); + extension.Initialize(context); + + // act + extension.Merge(context, convention); + + // assert + Assert.NotNull(convention.DefinitionAccessor); + Assert.Equal(2, convention.DefinitionAccessor!.Configurations.Count); + } + + [Fact] + public void Merge_Should_DeepMerge_EnumConfigurations() + { + // arrange + var convention = new MockSortConvention( + x => x.ConfigureEnum(d => d.Name("Foo"))); + var extension = new SortConventionExtension( + x => x.ConfigureEnum(d => d.Name("Foo"))); + var context = new ConventionContext( + "Scope", + new ServiceCollection().BuildServiceProvider(), + DescriptorContext.Create()); + + convention.Initialize(context); + extension.Initialize(context); + + // act + extension.Merge(context, convention); + + // assert + Assert.NotNull(convention.DefinitionAccessor); + List configuration = + Assert.Single(convention.DefinitionAccessor!.EnumConfigurations.Values)!; + Assert.Equal(2, configuration.Count); + } + + [Fact] + public void Merge_Should_Merge_EnumConfigurations() + { + // arrange + var convention = new MockSortConvention( + x => x.ConfigureEnum(d => d.Name("Foo"))); + var extension = new SortConventionExtension( + x => x.ConfigureEnum(d => d.Name("Foo"))); + var context = new ConventionContext( + "Scope", + new ServiceCollection().BuildServiceProvider(), + DescriptorContext.Create()); + + convention.Initialize(context); + extension.Initialize(context); + + // act + extension.Merge(context, convention); + + // assert + Assert.NotNull(convention.DefinitionAccessor); + Assert.Equal(2, convention.DefinitionAccessor!.EnumConfigurations.Count); + } + + [Fact] + public void Merge_Should_Merge_ProviderExtensionsTypes() + { + // arrange + var convention = + new MockSortConvention(x => x.AddProviderExtension()); + var extension = + new SortConventionExtension( + x => x.AddProviderExtension()); + var context = new ConventionContext( + "Scope", + new ServiceCollection().BuildServiceProvider(), + DescriptorContext.Create()); + + convention.Initialize(context); + extension.Initialize(context); + + // act + extension.Merge(context, convention); + + // assert + Assert.NotNull(convention.DefinitionAccessor); + Assert.Equal(2, convention.DefinitionAccessor!.ProviderExtensionsTypes.Count); + } + + [Fact] + public void Merge_Should_Merge_ProviderExtensions() + { + // arrange + var provider1 = new MockProviderExtensions(); + var convention = new MockSortConvention(x => x.AddProviderExtension(provider1)); + var provider2 = new MockProviderExtensions(); + var extension = new SortConventionExtension(x => x.AddProviderExtension(provider2)); + var context = new ConventionContext( + "Scope", + new ServiceCollection().BuildServiceProvider(), + DescriptorContext.Create()); + + convention.Initialize(context); + extension.Initialize(context); + + // act + extension.Merge(context, convention); + + // assert + Assert.NotNull(convention.DefinitionAccessor); + Assert.Collection( + convention.DefinitionAccessor!.ProviderExtensions, + x => Assert.Equal(provider1, x), + x => Assert.Equal(provider2, x)); + } + + private class Foo + { + public string Bar { get; } + } + + private class Bar + { + public string Foo { get; } + } + + private class MockSortEnumType : DefaultSortEnumType + { + } + + private class MockProviderExtensions : SortProviderExtensions + { + } + + private class MockProvider : ISortProvider + { + public IReadOnlyCollection FieldHandlers { get; } = null!; + public IReadOnlyCollection OperationHandlers { get; } = null!; + + public FieldMiddleware CreateExecutor(NameString argumentName) + { + throw new NotImplementedException(); + } + + public void ConfigureField(NameString argumentName, IObjectFieldDescriptor descriptor) + { + throw new NotImplementedException(); + } + } + + private class MockSortConvention : SortConvention + { + public MockSortConvention( + Action configure) + : base(configure) + { + } + + public SortConventionDefinition? DefinitionAccessor => base.Definition; + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Sorting.Tests/SortProviderExtensionsTests.cs b/src/HotChocolate/Data/test/Data.Sorting.Tests/SortProviderExtensionsTests.cs new file mode 100644 index 00000000000..47cd5342b43 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Sorting.Tests/SortProviderExtensionsTests.cs @@ -0,0 +1,99 @@ +using System; +using HotChocolate.Data.Sorting; +using HotChocolate.Data.Sorting.Expressions; +using HotChocolate.Resolvers; +using HotChocolate.Types.Descriptors; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace HotChocolate.Data +{ + public class SortProviderExtensionsTests + { + [Fact] + public void Merge_Should_Merge_OperationHandlersAndPrependExtensionHandlers() + { + // arrange + var firstFieldHandler = new QueryableAscendingSortOperationHandler(); + var extensionFieldHandler = new QueryableDescendingSortOperationHandler(); + var convention = new MockProvider(x => x.AddOperationHandler(firstFieldHandler)); + var extension = new MockProviderExtensions( + x => x.AddOperationHandler(extensionFieldHandler)); + var context = new ConventionContext( + "Scope", + new ServiceCollection().BuildServiceProvider(), + DescriptorContext.Create()); + + convention.Initialize(context); + extension.Initialize(context); + + // act + extension.Merge(context, convention); + + // assert + Assert.NotNull(convention.DefinitionAccessor); + Assert.Collection( + convention.DefinitionAccessor!.OperationHandlers, + x => Assert.Equal(extensionFieldHandler, x.HandlerInstance), + x => Assert.Equal(firstFieldHandler, x.HandlerInstance)); + } + + [Fact] + public void Merge_Should_Merge_HandlersAndPrependExtensionHandlers() + { + // arrange + var firstFieldHandler = new QueryableDefaultSortFieldHandler(); + var extensionFieldHandler = new MockFieldHandler(); + var convention = new MockProvider(x => x.AddFieldHandler(firstFieldHandler)); + var extension = new MockProviderExtensions( + x => x.AddFieldHandler(extensionFieldHandler)); + var context = new ConventionContext( + "Scope", + new ServiceCollection().BuildServiceProvider(), + DescriptorContext.Create()); + + convention.Initialize(context); + extension.Initialize(context); + + // act + extension.Merge(context, convention); + + // assert + Assert.NotNull(convention.DefinitionAccessor); + Assert.Collection( + convention.DefinitionAccessor!.Handlers, + x => Assert.Equal(extensionFieldHandler, x.HandlerInstance), + x => Assert.Equal(firstFieldHandler, x.HandlerInstance)); + } + + private class MockFieldHandler : QueryableDefaultSortFieldHandler + { + + } + + private class MockProviderExtensions + : SortProviderExtensions + { + public MockProviderExtensions( + Action> configure) + : base(configure) + { + } + } + + private class MockProvider : SortProvider + { + public SortProviderDefinition? DefinitionAccessor => base.Definition; + + public MockProvider(Action> configure) + : base(configure) + { + } + + public override FieldMiddleware CreateExecutor(NameString argumentName) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Sorting.Tests/SortTypeAttributeTests.cs b/src/HotChocolate/Data/test/Data.Sorting.Tests/SortTypeAttributeTests.cs index 603fa57bc8a..945462b007b 100644 --- a/src/HotChocolate/Data/test/Data.Sorting.Tests/SortTypeAttributeTests.cs +++ b/src/HotChocolate/Data/test/Data.Sorting.Tests/SortTypeAttributeTests.cs @@ -61,7 +61,7 @@ public class GenericTypeSortAttribute { public static string TypeName { get; } = "ThisIsATest"; - protected override void TryConfigure( + protected internal override void TryConfigure( IDescriptorContext context, IDescriptor d, ICustomAttributeProvider element) @@ -78,14 +78,14 @@ public class SortFieldAttributeTest { public static string Field { get; } = "FieldField"; - protected override void TryConfigure( + protected internal override void TryConfigure( IDescriptorContext context, IDescriptor descriptor, ICustomAttributeProvider element) { - if (descriptor is SortFieldDescriptor SortFieldDescriptor) + if (descriptor is SortFieldDescriptor sortFieldDescriptor) { - SortFieldDescriptor.Name(Field); + sortFieldDescriptor.Name(Field); } } }