diff --git a/src/HotChocolate/Data/src/Data/Filters/Convention/FilterConvention.cs b/src/HotChocolate/Data/src/Data/Filters/Convention/FilterConvention.cs index fa6dc7d0454..906a25c10a9 100644 --- a/src/HotChocolate/Data/src/Data/Filters/Convention/FilterConvention.cs +++ b/src/HotChocolate/Data/src/Data/Filters/Convention/FilterConvention.cs @@ -297,9 +297,29 @@ protected bool TryCreateFilterType( return true; } + // try to match open generic types if runtime type is a generic type + if (runtimeType.Source is { IsGenericType: true, IsGenericTypeDefinition: false }) + { + var genericTypeDefinition = runtimeType.Source.GetGenericTypeDefinition(); + if (_bindings.TryGetValue(genericTypeDefinition, out var genericFilterType)) + { + // if the filter type is a generic type definition, make it concrete + if (genericFilterType.IsGenericTypeDefinition) + { + var typeArguments = runtimeType.Source.GetGenericArguments(); + type = genericFilterType.MakeGenericType(typeArguments); + return true; + } + + // use the non-generic filter type directly + type = genericFilterType; + return true; + } + } + if (runtimeType.IsArrayOrList) { - if (runtimeType.ElementType is { } + if (runtimeType.ElementType is not null && TryCreateFilterType(runtimeType.ElementType, out var elementType)) { type = typeof(ListFilterInputType<>).MakeGenericType(elementType); 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 7d02ba39980..82141260f8b 100644 --- a/src/HotChocolate/Data/test/Data.Filters.Tests/Convention/FilterConventionTests.cs +++ b/src/HotChocolate/Data/test/Data.Filters.Tests/Convention/FilterConventionTests.cs @@ -31,7 +31,7 @@ public void FilterConvention_Should_Work_When_ConfigurationIsComplete() var value = Utf8GraphQLParser.Syntax.ParseValueLiteral("{ bar: { eq:\"a\" }}"); var type = new FooFilterInput(); - //act + // act CreateSchemaWith(type, convention); var executor = new ExecutorBuilder(type); @@ -87,7 +87,7 @@ public void FilterConvention_Should_Fail_When_FieldHandlerIsNotRegistered() var type = new FooFilterInput(); - //act + // act var error = Assert.Throws(() => CreateSchemaWith(type, convention)); // assert @@ -96,7 +96,7 @@ public void FilterConvention_Should_Fail_When_FieldHandlerIsNotRegistered() } [Fact] - public void FilterConvention_Should_Fail_When_OperationsInUknown() + public void FilterConvention_Should_Fail_When_OperationsInUnknown() { // arrange var provider = new QueryableFilterProvider( @@ -115,7 +115,7 @@ public void FilterConvention_Should_Fail_When_OperationsInUknown() var type = new FooFilterInput(); - //act + // act var error = Assert.Throws(() => CreateSchemaWith(type, convention)); // assert @@ -144,7 +144,7 @@ public void FilterConvention_Should_Fail_When_OperationsIsNotNamed() var type = new FooFilterInput(); - //act + // act Assert.Throws(() => CreateSchemaWith(type, convention)); } @@ -161,7 +161,7 @@ public void FilterConvention_Should_Fail_When_NoProviderWasRegistered() var type = new FooFilterInput(); - //act + // act var error = Assert.Throws(() => CreateSchemaWith(type, convention)); Assert.Single(error.Errors); @@ -188,7 +188,7 @@ public void FilterConvention_Should_Fail_When_NoMatchingBindingWasFound() var type = new FooFilterInput(); - //act + // act var error = Assert.Throws(() => CreateSchemaWith(type, convention)); // assert @@ -208,7 +208,7 @@ public void FilterConvention_Should_Work_With_Extensions() }); var convention = new FilterConvention( - descriptor => + _ => { }); @@ -225,7 +225,7 @@ public void FilterConvention_Should_Work_With_Extensions() var value = Utf8GraphQLParser.Syntax.ParseValueLiteral("{ bar: { eq:\"a\" }}"); var type = new FooFilterInput(); - //act + // act CreateSchemaWith(type, convention, extension1, extension2); var executor = new ExecutorBuilder(type); @@ -260,7 +260,7 @@ public void FilterConvention_Should_Work_With_ExtensionsType() var value = Utf8GraphQLParser.Syntax.ParseValueLiteral("{ bar: { eq:\"a\" }}"); var type = new FooFilterInput(); - //act + // act CreateSchemaWithTypes( type, convention, @@ -301,7 +301,7 @@ public void FilterConvention_Should_Work_With_ProviderExtensionsType() var value = Utf8GraphQLParser.Syntax.ParseValueLiteral("{ bar: { eq:\"a\" }}"); var type = new FooFilterInput(); - //act + // act CreateSchemaWith(type, convention, extension1); var executor = new ExecutorBuilder(type); @@ -334,7 +334,7 @@ public async Task FilterConvention_Should_UseBoundFilterType() .AddQueryType( x => x.Name("Query").Field("foos").UseFiltering().Resolve(new List())); - //act + // act var schema = await builder.BuildSchemaAsync(); // assert @@ -379,7 +379,7 @@ public async Task FilterConvention_Should_NotAddAnd() .AddQueryType( x => x.Name("Query").Field("foos").UseFiltering().Resolve(new List())); - //act + // act var schema = await builder.BuildSchemaAsync(); // assert @@ -404,7 +404,32 @@ public async Task FilterConvention_Should_NotAddOr() .AddQueryType( x => x.Name("Query").Field("foos").UseFiltering().Resolve(new List())); - //act + // act + var schema = await builder.BuildSchemaAsync(); + + // assert + schema.MatchSnapshot(); + } + + [Fact] + public async Task FilterConvention_Should_Support_OpenGeneric_RuntimeType_Binding() + { + // arrange + var convention = new FilterConvention( + descriptor => + { + descriptor.AddDefaults(); + descriptor.BindRuntimeType(typeof(GenericFoo<>), typeof(GenericFooOperationFilterInput)); + }); + + var builder = new ServiceCollection() + .AddGraphQL() + .AddConvention(convention) + .AddFiltering() + .AddQueryType( + x => x.Name("Query").Field("foos").UseFiltering().Resolve(new List())); + + // act var schema = await builder.BuildSchemaAsync(); // assert @@ -461,8 +486,7 @@ protected Schema CreateSchemaWith( public class MockFilterProviderExtensionConvention : QueryableFilterProviderExtension { - protected override void Configure( - IFilterProviderDescriptor descriptor) + protected override void Configure(IFilterProviderDescriptor descriptor) { descriptor.AddFieldHandler(); } @@ -485,8 +509,7 @@ protected override void Configure(IFilterInputTypeDescriptor descriptor) } } - public class FailingCombinator - : FilterOperationCombinator, string> + public class FailingCombinator : FilterOperationCombinator, string> { public override bool TryCombineOperations( FilterVisitorContext context, @@ -503,11 +526,19 @@ public class Foo public string Bar { get; set; } = null!; } - public class FooFilterInput - : FilterInputType + public class GenericFoo { - protected override void Configure( - IFilterInputTypeDescriptor descriptor) + public string Bar { get; set; } = null!; + } + + public class UsingGenericFoo + { + public GenericFoo Bar { get; set; } = null!; + } + + public class FooFilterInput : FilterInputType + { + protected override void Configure(IFilterInputTypeDescriptor descriptor) { descriptor.Field(t => t.Bar); descriptor.AllowAnd(false).AllowOr(false); @@ -515,4 +546,13 @@ protected override void Configure( } public class CustomFooFilterInput : FilterInputType; + + public class GenericFooOperationFilterInput : StringOperationFilterInputType + { + protected override void Configure(IFilterInputTypeDescriptor descriptor) + { + descriptor.Operation(DefaultFilterOperations.Equals).Type(); + descriptor.AllowAnd(false).AllowOr(false); + } + } } diff --git a/src/HotChocolate/Data/test/Data.Filters.Tests/Convention/__snapshots__/FilterConventionTests.FilterConvention_Should_Fail_When_OperationsInUknown.snap b/src/HotChocolate/Data/test/Data.Filters.Tests/Convention/__snapshots__/FilterConventionTests.FilterConvention_Should_Fail_When_OperationsInUnknown.snap similarity index 100% rename from src/HotChocolate/Data/test/Data.Filters.Tests/Convention/__snapshots__/FilterConventionTests.FilterConvention_Should_Fail_When_OperationsInUknown.snap rename to src/HotChocolate/Data/test/Data.Filters.Tests/Convention/__snapshots__/FilterConventionTests.FilterConvention_Should_Fail_When_OperationsInUnknown.snap diff --git a/src/HotChocolate/Data/test/Data.Filters.Tests/Convention/__snapshots__/FilterConventionTests.FilterConvention_Should_Support_OpenGeneric_RuntimeType_Binding.graphql b/src/HotChocolate/Data/test/Data.Filters.Tests/Convention/__snapshots__/FilterConventionTests.FilterConvention_Should_Support_OpenGeneric_RuntimeType_Binding.graphql new file mode 100644 index 00000000000..578fbff9dc3 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Filters.Tests/Convention/__snapshots__/FilterConventionTests.FilterConvention_Should_Support_OpenGeneric_RuntimeType_Binding.graphql @@ -0,0 +1,25 @@ +schema { + query: Query +} + +type GenericFooOfFoo { + bar: String! +} + +type Query { + foos(where: UsingGenericFooFilterInput): [UsingGenericFoo] +} + +type UsingGenericFoo { + bar: GenericFooOfFoo! +} + +input GenericFooOperationFilterInput { + eq: String +} + +input UsingGenericFooFilterInput { + and: [UsingGenericFooFilterInput!] + or: [UsingGenericFooFilterInput!] + bar: GenericFooOperationFilterInput +}