diff --git a/src/Core/Types.Filters.Tests/ComparableFilterInputTypeTests.cs b/src/Core/Types.Filters.Tests/ComparableFilterInputTypeTests.cs index 929b024fda7..6c11a58851c 100644 --- a/src/Core/Types.Filters.Tests/ComparableFilterInputTypeTests.cs +++ b/src/Core/Types.Filters.Tests/ComparableFilterInputTypeTests.cs @@ -28,7 +28,8 @@ public void Create_Explitcit_Filters() // act var schema = CreateSchema(new FilterInputType(descriptor => { - descriptor.Filter(x => x.BarShort).BindExplicitly().AllowEquals() + descriptor.Filter(x => x.BarShort) + .BindExplicitly().AllowEquals() .And().AllowNotEquals() .And().AllowIn() .And().AllowNotIn() diff --git a/src/Core/Types.Filters.Tests/QueryableFilterTests.cs b/src/Core/Types.Filters.Tests/QueryableFilterTests.cs index a501c926919..331f8161156 100644 --- a/src/Core/Types.Filters.Tests/QueryableFilterTests.cs +++ b/src/Core/Types.Filters.Tests/QueryableFilterTests.cs @@ -38,6 +38,24 @@ public void Execute_Filter() result.MatchSnapshot(); } + [Fact] + public void Infer_Filter_From_Field() + { + // arrange + ISchema schema = SchemaBuilder.New() + .AddQueryType(d => d.Field(t => t.Foos).UseFilter()) + .Create(); + + IQueryExecutor executor = schema.MakeExecutable(); + + // act + IExecutionResult result = executor.Execute( + "{ foos(where: { bar_starts_with: \"a\" }) { bar } }"); + + // assert + result.MatchSnapshot(); + } + public class QueryType : ObjectType diff --git a/src/Core/Types.Filters.Tests/QueryableFilterVisitorBooleanTests.cs b/src/Core/Types.Filters.Tests/QueryableFilterVisitorBooleanTests.cs new file mode 100644 index 00000000000..0334deef0a2 --- /dev/null +++ b/src/Core/Types.Filters.Tests/QueryableFilterVisitorBooleanTests.cs @@ -0,0 +1,76 @@ +using System; +using HotChocolate.Language; +using HotChocolate.Utilities; +using Xunit; + +namespace HotChocolate.Types.Filters +{ + public class QueryableFilterVisitorBooleanTests + : TypeTestBase + { + [Fact] + public void Create_BooleanEqual_Expression() + { + // arrange + var value = new ObjectValueNode( + new ObjectFieldNode("bar", + new BooleanValueNode(true))); + + var fooType = CreateType(new FooFilterType()); + + // act + var filter = new QueryableFilterVisitor( + fooType, typeof(Foo), TypeConversion.Default); + value.Accept(filter); + Func func = filter.CreateFilter().Compile(); + + // assert + var a = new Foo { Bar = true }; + Assert.True(func(a)); + + var b = new Foo { Bar = false }; + Assert.False(func(b)); + } + + + [Fact] + public void Create_BooleanNotEqual_Expression() + { + // arrange + var value = new ObjectValueNode( + new ObjectFieldNode("bar", + new BooleanValueNode(false))); + + var fooType = CreateType(new FooFilterType()); + + // act + var filter = new QueryableFilterVisitor( + fooType, typeof(Foo), TypeConversion.Default); + value.Accept(filter); + Func func = filter.CreateFilter().Compile(); + + // assert + var a = new Foo { Bar = false }; + Assert.True(func(a)); + + var b = new Foo { Bar = true }; + Assert.False(func(b)); + } + + public class Foo + { + public bool Bar { get; set; } + } + + public class FooFilterType + : FilterInputType + { + protected override void Configure( + IFilterInputTypeDescriptor descriptor) + { + descriptor.Filter(t => t.Bar) + .AllowEquals().And().AllowNotEquals(); + } + } + } +} diff --git a/src/Core/Types.Filters.Tests/QueryableFilterVisitorComparableTests.cs b/src/Core/Types.Filters.Tests/QueryableFilterVisitorComparableTests.cs new file mode 100644 index 00000000000..d8a4535125d --- /dev/null +++ b/src/Core/Types.Filters.Tests/QueryableFilterVisitorComparableTests.cs @@ -0,0 +1,361 @@ +using System; +using HotChocolate.Language; +using HotChocolate.Utilities; +using Xunit; + +namespace HotChocolate.Types.Filters +{ + public class QueryableFilterVisitorComparableTests + : TypeTestBase + { + + [Fact] + public void Create_ShortEqual_Expression() + { + // arrange + var value = new ObjectValueNode( + new ObjectFieldNode("barShort", + new IntValueNode("12"))); + + var fooType = CreateType(new FooFilterType()); + + // act + var filter = new QueryableFilterVisitor( + fooType, typeof(Foo), TypeConversion.Default); + value.Accept(filter); + Func func = filter.CreateFilter().Compile(); + + // assert + var a = new Foo { BarShort = 12 }; + Assert.True(func(a)); + + var b = new Foo { BarShort = 13 }; + Assert.False(func(b)); + } + + [Fact] + public void Create_ShortNotEqual_Expression() + { + // arrange + var value = new ObjectValueNode( + new ObjectFieldNode("barShort_not", + new IntValueNode("12"))); + + var fooType = CreateType(new FooFilterType()); + + // act + var filter = new QueryableFilterVisitor( + fooType, typeof(Foo), TypeConversion.Default); + value.Accept(filter); + Func func = filter.CreateFilter().Compile(); + + // assert + var a = new Foo { BarShort = 13 }; + Assert.True(func(a)); + + var b = new Foo { BarShort = 12 }; + Assert.False(func(b)); + } + + + [Fact] + public void Create_ShortGreaterThan_Expression() + { + // arrange + var value = new ObjectValueNode( + new ObjectFieldNode("barShort_gt", + new IntValueNode("12"))); + + var fooType = CreateType(new FooFilterType()); + + // act + var filter = new QueryableFilterVisitor( + fooType, typeof(Foo), TypeConversion.Default); + value.Accept(filter); + Func func = filter.CreateFilter().Compile(); + + // assert + var a = new Foo { BarShort = 11 }; + Assert.False(func(a)); + + var b = new Foo { BarShort = 12 }; + Assert.False(func(b)); + + var c = new Foo { BarShort = 13 }; + Assert.True(func(c)); + } + + [Fact] + public void Create_ShortNotGreaterThan_Expression() + { + // arrange + var value = new ObjectValueNode( + new ObjectFieldNode("barShort_not_gt", + new IntValueNode("12"))); + + var fooType = CreateType(new FooFilterType()); + + // act + var filter = new QueryableFilterVisitor( + fooType, typeof(Foo), TypeConversion.Default); + value.Accept(filter); + Func func = filter.CreateFilter().Compile(); + + // assert + var a = new Foo { BarShort = 11 }; + Assert.True(func(a)); + + var b = new Foo { BarShort = 12 }; + Assert.True(func(b)); + + var c = new Foo { BarShort = 13 }; + Assert.False(func(c)); + } + + + [Fact] + public void Create_ShortGreaterThanOrEquals_Expression() + { + // arrange + var value = new ObjectValueNode( + new ObjectFieldNode("barShort_gte", + new IntValueNode("12"))); + + var fooType = CreateType(new FooFilterType()); + + // act + var filter = new QueryableFilterVisitor( + fooType, typeof(Foo), TypeConversion.Default); + value.Accept(filter); + Func func = filter.CreateFilter().Compile(); + + // assert + var a = new Foo { BarShort = 11 }; + Assert.False(func(a)); + + var b = new Foo { BarShort = 12 }; + Assert.True(func(b)); + + var c = new Foo { BarShort = 13 }; + Assert.True(func(c)); + } + + [Fact] + public void Create_ShortNotGreaterThanOrEquals_Expression() + { + // arrange + var value = new ObjectValueNode( + new ObjectFieldNode("barShort_not_gte", + new IntValueNode("12"))); + + var fooType = CreateType(new FooFilterType()); + + // act + var filter = new QueryableFilterVisitor( + fooType, typeof(Foo), TypeConversion.Default); + value.Accept(filter); + Func func = filter.CreateFilter().Compile(); + + // assert + var a = new Foo { BarShort = 11 }; + Assert.True(func(a)); + + var b = new Foo { BarShort = 12 }; + Assert.False(func(b)); + + var c = new Foo { BarShort = 13 }; + Assert.False(func(c)); + } + + + + [Fact] + public void Create_ShortLowerThan_Expression() + { + // arrange + var value = new ObjectValueNode( + new ObjectFieldNode("barShort_lt", + new IntValueNode("12"))); + + var fooType = CreateType(new FooFilterType()); + + // act + var filter = new QueryableFilterVisitor( + fooType, typeof(Foo), TypeConversion.Default); + value.Accept(filter); + Func func = filter.CreateFilter().Compile(); + + // assert + var a = new Foo { BarShort = 11 }; + Assert.True(func(a)); + + var b = new Foo { BarShort = 12 }; + Assert.False(func(b)); + + var c = new Foo { BarShort = 13 }; + Assert.False(func(c)); + } + + [Fact] + public void Create_ShortNotLowerThan_Expression() + { + // arrange + var value = new ObjectValueNode( + new ObjectFieldNode("barShort_not_lt", + new IntValueNode("12"))); + + var fooType = CreateType(new FooFilterType()); + + // act + var filter = new QueryableFilterVisitor( + fooType, typeof(Foo), TypeConversion.Default); + value.Accept(filter); + Func func = filter.CreateFilter().Compile(); + + // assert + var a = new Foo { BarShort = 11 }; + Assert.False(func(a)); + + var b = new Foo { BarShort = 12 }; + Assert.True(func(b)); + + var c = new Foo { BarShort = 13 }; + Assert.True(func(c)); + } + + + [Fact] + public void Create_ShortLowerThanOrEquals_Expression() + { + // arrange + var value = new ObjectValueNode( + new ObjectFieldNode("barShort_lte", + new IntValueNode("12"))); + + var fooType = CreateType(new FooFilterType()); + + // act + var filter = new QueryableFilterVisitor( + fooType, typeof(Foo), TypeConversion.Default); + value.Accept(filter); + Func func = filter.CreateFilter().Compile(); + + // assert + var a = new Foo { BarShort = 11 }; + Assert.True(func(a)); + + var b = new Foo { BarShort = 12 }; + Assert.True(func(b)); + + var c = new Foo { BarShort = 13 }; + Assert.False(func(c)); + } + + [Fact] + public void Create_ShortNotLowerThanOrEquals_Expression() + { + // arrange + var value = new ObjectValueNode( + new ObjectFieldNode("barShort_not_lte", + new IntValueNode("12"))); + + var fooType = CreateType(new FooFilterType()); + + // act + var filter = new QueryableFilterVisitor( + fooType, typeof(Foo), TypeConversion.Default); + value.Accept(filter); + Func func = filter.CreateFilter().Compile(); + + // assert + var a = new Foo { BarShort = 11 }; + Assert.False(func(a)); + + var b = new Foo { BarShort = 12 }; + Assert.False(func(b)); + + var c = new Foo { BarShort = 13 }; + Assert.True(func(c)); + } + + [Fact] + public void Create_ShortIn_Expression() + { + // arrange + var value = new ObjectValueNode( + new ObjectFieldNode("barShort_in", + new ListValueNode(new[] + { + new IntValueNode("13"), + new IntValueNode("14") + })) + ); + + var fooType = CreateType(new FooFilterType()); + + // act + var filter = new QueryableFilterVisitor( + fooType, typeof(Foo), TypeConversion.Default); + value.Accept(filter); + Func func = filter.CreateFilter().Compile(); + + // assert + var a = new Foo { BarShort = 13 }; + Assert.True(func(a)); + + var b = new Foo { BarShort = 12 }; + Assert.False(func(b)); + } + + [Fact] + public void Create_ShortNotIn_Expression() + { + // arrange + var value = new ObjectValueNode( + new ObjectFieldNode("barShort_not_in", + new ListValueNode(new[] { new IntValueNode("13"), new IntValueNode("14") } + )) + ); + + var fooType = CreateType(new FooFilterType()); + + // act + var filter = new QueryableFilterVisitor(fooType, typeof(Foo), TypeConversion.Default); + value.Accept(filter); + Func func = filter.CreateFilter().Compile(); + + // assert + var a = new Foo { BarShort = 12 }; + Assert.True(func(a)); + + var b = new Foo { BarShort = 13 }; + Assert.False(func(b)); + } + + + public class Foo + { + public short BarShort { get; set; } + public int BarInt { get; set; } + public long BarLong { get; set; } + public float BarFloat { get; set; } + public double BarDouble { get; set; } + public decimal BarDecimal { get; set; } + } + + public class FooFilterType + : FilterInputType + { + protected override void Configure( + IFilterInputTypeDescriptor descriptor) + { + descriptor.Filter(x => x.BarShort); + descriptor.Filter(x => x.BarInt); + descriptor.Filter(x => x.BarLong); + descriptor.Filter(x => x.BarFloat); + descriptor.Filter(x => x.BarDouble); + descriptor.Filter(x => x.BarDecimal); + } + } + } +} diff --git a/src/Core/Types.Filters.Tests/QueryableFilterVisitorTests.cs b/src/Core/Types.Filters.Tests/QueryableFilterVisitorStringTests.cs similarity index 59% rename from src/Core/Types.Filters.Tests/QueryableFilterVisitorTests.cs rename to src/Core/Types.Filters.Tests/QueryableFilterVisitorStringTests.cs index d31c78e7aa2..155409e77af 100644 --- a/src/Core/Types.Filters.Tests/QueryableFilterVisitorTests.cs +++ b/src/Core/Types.Filters.Tests/QueryableFilterVisitorStringTests.cs @@ -5,7 +5,7 @@ namespace HotChocolate.Types.Filters { - public class QueryableFilterVisitorTests + public class QueryableFilterVisitorStringTests : TypeTestBase { [Fact] @@ -60,6 +60,118 @@ public void Create_StringNotEqual_Expression() Assert.True(func(b)); } + [Fact] + public void Create_StringIn_Expression() + { + // arrange + var value = new ObjectValueNode( + new ObjectFieldNode("bar_in", + new ListValueNode(new[] + { + new StringValueNode("a"), + new StringValueNode("c") + }))); + + var fooType = CreateType(new FooFilterType()); + + // act + var filter = new QueryableFilterVisitor( + fooType, + typeof(Foo), + TypeConversion.Default); + value.Accept(filter); + Func func = filter.CreateFilter().Compile(); + + // assert + var a = new Foo { Bar = "a" }; + Assert.True(func(a)); + + var b = new Foo { Bar = "b" }; + Assert.False(func(b)); + } + + [Fact] + public void Create_StringNotIn_Expression() + { + // arrange + var value = new ObjectValueNode( + new ObjectFieldNode("bar_not_in", + new ListValueNode(new[] + { + new StringValueNode("a"), + new StringValueNode("c") + }))); + + var fooType = CreateType(new FooFilterType()); + + // act + var filter = new QueryableFilterVisitor( + fooType, + typeof(Foo), + TypeConversion.Default); + value.Accept(filter); + Func func = filter.CreateFilter().Compile(); + + // assert + var a = new Foo { Bar = "a" }; + Assert.False(func(a)); + + var b = new Foo { Bar = "b" }; + Assert.True(func(b)); + } + + [Fact] + public void Create_StringContains_Expression() + { + // arrange + var value = new ObjectValueNode( + new ObjectFieldNode("bar_contains", + new StringValueNode("a"))); + + var fooType = CreateType(new FooFilterType()); + + // act + var filter = new QueryableFilterVisitor( + fooType, + typeof(Foo), + TypeConversion.Default); + value.Accept(filter); + Func func = filter.CreateFilter().Compile(); + + // assert + var a = new Foo { Bar = "testatest" }; + Assert.True(func(a)); + + var b = new Foo { Bar = "testbtest" }; + Assert.False(func(b)); + } + + [Fact] + public void Create_StringNoContains_Expression() + { + // arrange + var value = new ObjectValueNode( + new ObjectFieldNode("bar_not_contains", + new StringValueNode("a"))); + + var fooType = CreateType(new FooFilterType()); + + // act + var filter = new QueryableFilterVisitor( + fooType, + typeof(Foo), + TypeConversion.Default); + value.Accept(filter); + Func func = filter.CreateFilter().Compile(); + + // assert + var a = new Foo { Bar = "testatest" }; + Assert.False(func(a)); + + var b = new Foo { Bar = "testbtest" }; + Assert.True(func(b)); + } + [Fact] public void Create_StringStartsWith_Expression() { @@ -175,6 +287,8 @@ public class FooFilterType protected override void Configure( IFilterInputTypeDescriptor descriptor) { + descriptor.Filter(t => t.Bar) + .BindImplicitly(); } } } diff --git a/src/Core/Types.Filters.Tests/__snapshots__/BooleanFilterInputTypeTests.Create_Explitcit_Filters.snap b/src/Core/Types.Filters.Tests/__snapshots__/BooleanFilterInputTypeTests.Create_Explitcit_Filters.snap index 8c5c50b6d62..9c4644e1c94 100644 --- a/src/Core/Types.Filters.Tests/__snapshots__/BooleanFilterInputTypeTests.Create_Explitcit_Filters.snap +++ b/src/Core/Types.Filters.Tests/__snapshots__/BooleanFilterInputTypeTests.Create_Explitcit_Filters.snap @@ -1,4 +1,4 @@ -schema { +schema { query: Query } diff --git a/src/Core/Types.Filters.Tests/__snapshots__/QueryableFilterTests.Infer_Filter_From_Field.snap b/src/Core/Types.Filters.Tests/__snapshots__/QueryableFilterTests.Infer_Filter_From_Field.snap new file mode 100644 index 00000000000..fcfa3be45f4 --- /dev/null +++ b/src/Core/Types.Filters.Tests/__snapshots__/QueryableFilterTests.Infer_Filter_From_Field.snap @@ -0,0 +1,21 @@ +{ + "Data": { + "foos": [ + { + "bar": "aa" + }, + { + "bar": "ab" + }, + { + "bar": "ac" + }, + { + "bar": "ad" + } + ] + }, + "Extensions": {}, + "Errors": [], + "ContextData": {} +} diff --git a/src/Core/Types.Filters.Tests/__snapshots__/StringFilterInputTypeTests.Create_Explicit_Filters.snap b/src/Core/Types.Filters.Tests/__snapshots__/StringFilterInputTypeTests.Create_Explicit_Filters.snap index 04372b75aed..6db32fb7002 100644 --- a/src/Core/Types.Filters.Tests/__snapshots__/StringFilterInputTypeTests.Create_Explicit_Filters.snap +++ b/src/Core/Types.Filters.Tests/__snapshots__/StringFilterInputTypeTests.Create_Explicit_Filters.snap @@ -1,4 +1,4 @@ -schema { +schema { query: Query } diff --git a/src/Core/Types.Filters.Tests/__snapshots__/StringFilterInputTypeTests.Create_Implicit_Filters.snap b/src/Core/Types.Filters.Tests/__snapshots__/StringFilterInputTypeTests.Create_Implicit_Filters.snap index f635ac8e789..4bbb970cf2f 100644 --- a/src/Core/Types.Filters.Tests/__snapshots__/StringFilterInputTypeTests.Create_Implicit_Filters.snap +++ b/src/Core/Types.Filters.Tests/__snapshots__/StringFilterInputTypeTests.Create_Implicit_Filters.snap @@ -1,4 +1,4 @@ -schema { +schema { query: Query } diff --git a/src/Core/Types.Filters.Tests/__snapshots__/StringFilterInputTypeTests.Define_Filters_By_Configure_Override.snap b/src/Core/Types.Filters.Tests/__snapshots__/StringFilterInputTypeTests.Define_Filters_By_Configure_Override.snap index 48329389e4e..7eeb25042ee 100644 --- a/src/Core/Types.Filters.Tests/__snapshots__/StringFilterInputTypeTests.Define_Filters_By_Configure_Override.snap +++ b/src/Core/Types.Filters.Tests/__snapshots__/StringFilterInputTypeTests.Define_Filters_By_Configure_Override.snap @@ -1,4 +1,4 @@ -schema { +schema { query: Query } diff --git a/src/Core/Types.Filters.Tests/__snapshots__/StringFilterInputTypeTests.Rename_Specific_Filter.snap b/src/Core/Types.Filters.Tests/__snapshots__/StringFilterInputTypeTests.Rename_Specific_Filter.snap index 84810ef19ec..db994fdef94 100644 --- a/src/Core/Types.Filters.Tests/__snapshots__/StringFilterInputTypeTests.Rename_Specific_Filter.snap +++ b/src/Core/Types.Filters.Tests/__snapshots__/StringFilterInputTypeTests.Rename_Specific_Filter.snap @@ -1,4 +1,4 @@ -schema { +schema { query: Query } diff --git a/src/Core/Types.Filters/Boolean/BooleanFilterFieldDescriptor.cs b/src/Core/Types.Filters/Boolean/BooleanFilterFieldDescriptor.cs index dc509babc81..7bc9cbabdb2 100644 --- a/src/Core/Types.Filters/Boolean/BooleanFilterFieldDescriptor.cs +++ b/src/Core/Types.Filters/Boolean/BooleanFilterFieldDescriptor.cs @@ -59,7 +59,7 @@ private BooleanFilterOperationDescriptor CreateOperation( FilterOperationKind operationKind) { var operation = new FilterOperation( - typeof(string), + typeof(bool), operationKind, Definition.Property); diff --git a/src/Core/Types.Filters/Comparable/ComparableFilterFieldDescriptor.cs b/src/Core/Types.Filters/Comparable/ComparableFilterFieldDescriptor.cs index f2ef542522e..7d77f5f0973 100644 --- a/src/Core/Types.Filters/Comparable/ComparableFilterFieldDescriptor.cs +++ b/src/Core/Types.Filters/Comparable/ComparableFilterFieldDescriptor.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Reflection; using HotChocolate.Types.Descriptors; @@ -154,7 +155,7 @@ private ComparableFilterOperationDescriptor CreateOperation( FilterOperationKind operationKind) { var operation = new FilterOperation( - typeof(string), + typeof(IComparable), operationKind, Definition.Property); diff --git a/src/Core/Types.Filters/Expressions/BooleanOperationHandler.cs b/src/Core/Types.Filters/Expressions/BooleanOperationHandler.cs new file mode 100644 index 00000000000..1794f678448 --- /dev/null +++ b/src/Core/Types.Filters/Expressions/BooleanOperationHandler.cs @@ -0,0 +1,46 @@ +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using HotChocolate.Language; +using HotChocolate.Utilities; + +namespace HotChocolate.Types.Filters.Expressions +{ + public class BooleanOperationHandler + : IExpressionOperationHandler + { + + public bool TryHandle( + FilterOperation operation, + IInputType type, + IValueNode value, + Expression instance, + ITypeConversion converter, + out Expression expression) + { + if (operation.Type == typeof(bool) && value is BooleanValueNode s) + { + MemberExpression property = + Expression.Property(instance, operation.Property); + + switch (operation.Kind) + { + case FilterOperationKind.Equals: + expression = FilterExpressionBuilder.Equals(property, s.Value); + return true; + + case FilterOperationKind.NotEquals: + expression = FilterExpressionBuilder.Not( + FilterExpressionBuilder.Equals(property, s.Value) + ); + return true; + + } + } + + expression = null; + return false; + } + + } +} diff --git a/src/Core/Types.Filters/Expressions/ComparableOperationHandler.cs b/src/Core/Types.Filters/Expressions/ComparableOperationHandler.cs new file mode 100644 index 00000000000..88a7715db6d --- /dev/null +++ b/src/Core/Types.Filters/Expressions/ComparableOperationHandler.cs @@ -0,0 +1,127 @@ +using System; +using System.Linq.Expressions; +using HotChocolate.Language; +using HotChocolate.Utilities; + +namespace HotChocolate.Types.Filters.Expressions +{ + public class ComparableOperationHandler + : IExpressionOperationHandler + { + public bool TryHandle( + FilterOperation operation, + IInputType type, + IValueNode value, + Expression instance, + ITypeConversion converter, + out Expression expression) + { + if (operation.Type == typeof(IComparable) + && value is IValueNode s) + { + MemberExpression property = + Expression.Property(instance, operation.Property); + var parsedValue = type.ParseLiteral(value); + + if (operation.Property.PropertyType + .IsInstanceOfType(parsedValue)) + { + parsedValue = converter.Convert( + typeof(object), + operation.Property.PropertyType, + parsedValue); + } + + switch (operation.Kind) + { + case FilterOperationKind.Equals: + expression = FilterExpressionBuilder.Equals( + property, parsedValue); + return true; + + case FilterOperationKind.NotEquals: + expression = FilterExpressionBuilder.Not( + FilterExpressionBuilder.Equals( + property, parsedValue) + ); + return true; + + case FilterOperationKind.GreaterThan: + expression = FilterExpressionBuilder.GreaterThan( + property, parsedValue); + return true; + + case FilterOperationKind.NotGreaterThan: + expression = FilterExpressionBuilder.Not( + FilterExpressionBuilder.GreaterThan( + property, parsedValue) + ); + return true; + + case FilterOperationKind.GreaterThanOrEqual: + expression = FilterExpressionBuilder.GreaterThanOrEqual( + property, parsedValue); + return true; + + case FilterOperationKind.NotGreaterThanOrEqual: + expression = FilterExpressionBuilder.Not( + FilterExpressionBuilder.GreaterThanOrEqual( + property, parsedValue) + ); + return true; + + case FilterOperationKind.LowerThan: + expression = FilterExpressionBuilder.LowerThan( + property, parsedValue); + return true; + + case FilterOperationKind.NotLowerThan: + expression = FilterExpressionBuilder.Not( + FilterExpressionBuilder.LowerThan( + property, parsedValue)); + return true; + + case FilterOperationKind.LowerThanOrEqual: + expression = FilterExpressionBuilder.LowerThanOrEqual( + property, parsedValue); + return true; + + case FilterOperationKind.NotLowerThanOrEqual: + expression = FilterExpressionBuilder.Not( + FilterExpressionBuilder.LowerThanOrEqual( + property, parsedValue)); + return true; + } + } + + if (operation.Type == typeof(IComparable) && value is ListValueNode li) + { + MemberExpression property = + Expression.Property(instance, operation.Property); + var parsedValue = type.ParseLiteral(value); + + switch (operation.Kind) + { + case FilterOperationKind.In: + expression = FilterExpressionBuilder.In( + property, + operation.Property.PropertyType, + parsedValue); + return true; + + case FilterOperationKind.NotIn: + expression = FilterExpressionBuilder.Not( + FilterExpressionBuilder.In( + property, + operation.Property.PropertyType, + parsedValue) + ); + return true; + } + } + + expression = null; + return false; + } + } +} diff --git a/src/Core/Types.Filters/Expressions/ExpressionOperationHandlers.cs b/src/Core/Types.Filters/Expressions/ExpressionOperationHandlers.cs index 88db385c264..9c5ab61d93b 100644 --- a/src/Core/Types.Filters/Expressions/ExpressionOperationHandlers.cs +++ b/src/Core/Types.Filters/Expressions/ExpressionOperationHandlers.cs @@ -7,7 +7,9 @@ internal static class ExpressionOperationHandlers public static IReadOnlyList All { get; } = new IExpressionOperationHandler[] { - new StringOperationHandler() + new StringOperationHandler(), + new BooleanOperationHandler(), + new ComparableOperationHandler() }; } } diff --git a/src/Core/Types.Filters/Expressions/FilterExpressionBuilder.cs b/src/Core/Types.Filters/Expressions/FilterExpressionBuilder.cs new file mode 100644 index 00000000000..197501ee1ee --- /dev/null +++ b/src/Core/Types.Filters/Expressions/FilterExpressionBuilder.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; + +namespace HotChocolate.Types.Filters.Expressions +{ + internal static class FilterExpressionBuilder + { + + private static readonly MethodInfo _startsWith = + typeof(string).GetMethods().Single(m => + m.Name.Equals("StartsWith") + && m.GetParameters().Length == 1 + && m.GetParameters().Single().ParameterType == typeof(string)); + + private static readonly MethodInfo _endsWith = + typeof(string).GetMethods().Single(m => + m.Name.Equals("EndsWith") + && m.GetParameters().Length == 1 + && m.GetParameters().Single().ParameterType == typeof(string)); + + + private static readonly MethodInfo _contains = + typeof(string).GetMethods().Single(m => + m.Name.Equals("Contains") + && m.GetParameters().Length == 1 + && m.GetParameters().Single().ParameterType == typeof(string)); + + public static Expression Not(Expression expression) + { + return Expression.Equal(expression, Expression.Constant(false)); + } + + public static Expression Equals(MemberExpression property, object value) + { + return Expression.Equal(property, Expression.Constant(value)); + } + + public static Expression In(MemberExpression property, Type genericType, object parsedValue) + { + return Expression.Call( + typeof(Enumerable), + "Contains", + new Type[] { genericType }, + Expression.Constant(parsedValue), + property + ); + } + + public static Expression GreaterThan(MemberExpression property, object value) + { + return Expression.GreaterThan(property, Expression.Constant(value)); + } + + public static Expression GreaterThanOrEqual(MemberExpression property, object value) + { + return Expression.GreaterThanOrEqual(property, Expression.Constant(value)); + } + + public static Expression LowerThan(MemberExpression property, object value) + { + return Expression.LessThan(property, Expression.Constant(value)); + } + + public static Expression LowerThanOrEqual(MemberExpression property, object value) + { + return Expression.LessThanOrEqual(property, Expression.Constant(value)); + } + + public static Expression StartsWith(MemberExpression property, object value) + { + return Expression.Call(property, _startsWith, new[] { Expression.Constant(value) }); + } + + public static Expression EndsWith(MemberExpression property, object value) + { + return Expression.Call(property, _endsWith, new[] { Expression.Constant(value) }); + } + public static Expression Contains(MemberExpression property, object value) + { + return Expression.Call(property, _contains, new[] { Expression.Constant(value) }); + } + } +} diff --git a/src/Core/Types.Filters/Expressions/StringOperationHandler.cs b/src/Core/Types.Filters/Expressions/StringOperationHandler.cs index 26d469a9104..a1714096e0c 100644 --- a/src/Core/Types.Filters/Expressions/StringOperationHandler.cs +++ b/src/Core/Types.Filters/Expressions/StringOperationHandler.cs @@ -1,6 +1,4 @@ -using System.Linq; using System.Linq.Expressions; -using System.Reflection; using HotChocolate.Language; using HotChocolate.Utilities; @@ -9,17 +7,6 @@ namespace HotChocolate.Types.Filters.Expressions public class StringOperationHandler : IExpressionOperationHandler { - private static readonly MethodInfo _startsWith = - typeof(string).GetMethods().Single(m => - m.Name.Equals("StartsWith") - && m.GetParameters().Length == 1 - && m.GetParameters().Single().ParameterType == typeof(string)); - - private static readonly MethodInfo _endsWith = - typeof(string).GetMethods().Single(m => - m.Name.Equals("EndsWith") - && m.GetParameters().Length == 1 - && m.GetParameters().Single().ParameterType == typeof(string)); public bool TryHandle( FilterOperation operation, @@ -37,49 +24,73 @@ public bool TryHandle( switch (operation.Kind) { case FilterOperationKind.Equals: - expression = Expression.Equal( - property, - Expression.Constant(s.Value)); + expression = FilterExpressionBuilder.Equals( + property, s.Value); return true; case FilterOperationKind.NotEquals: - expression = Expression.Equal( - Expression.Equal( - property, - Expression.Constant(s.Value)), - Expression.Constant(false)); + expression = FilterExpressionBuilder.Not( + FilterExpressionBuilder.Equals(property, s.Value) + ); return true; case FilterOperationKind.StartsWith: - expression = Expression.Call( - property, - _startsWith, - new[] { Expression.Constant(s.Value) }); + expression = FilterExpressionBuilder.StartsWith( + property, s.Value); return true; case FilterOperationKind.EndsWith: - expression = Expression.Call( - property, - _endsWith, - new[] { Expression.Constant(s.Value) }); + expression = FilterExpressionBuilder.EndsWith( + property, s.Value); return true; case FilterOperationKind.NotStartsWith: - expression = Expression.Equal( - Expression.Call( - property, - _startsWith, - new[] { Expression.Constant(s.Value) }), - Expression.Constant(false)); + expression = FilterExpressionBuilder.Not( + FilterExpressionBuilder.StartsWith( + property, s.Value) + ); return true; case FilterOperationKind.NotEndsWith: - expression = Expression.Equal( - Expression.Call( + expression = FilterExpressionBuilder.Not( + FilterExpressionBuilder.EndsWith(property, s.Value) + ); + return true; + + case FilterOperationKind.Contains: + expression = FilterExpressionBuilder.Contains( + property, s.Value); + return true; + + case FilterOperationKind.NotContains: + expression = FilterExpressionBuilder.Not( + FilterExpressionBuilder.Contains(property, s.Value) + ); + return true; + } + } + + if (operation.Type == typeof(string) && value is ListValueNode li) + { + MemberExpression property = + Expression.Property(instance, operation.Property); + var parsedValue = type.ParseLiteral(value); + + switch (operation.Kind) + { + case FilterOperationKind.In: + expression = FilterExpressionBuilder.In( + property, + operation.Property.PropertyType, + parsedValue); + return true; + + case FilterOperationKind.NotIn: + expression = FilterExpressionBuilder.Not( + FilterExpressionBuilder.In( property, - _endsWith, - new[] { Expression.Constant(s.Value) }), - Expression.Constant(false)); + operation.Property.PropertyType, + parsedValue)); return true; } } diff --git a/src/Core/Types.Filters/Extensions/FilterObjectFieldDescriptorExtensions.cs b/src/Core/Types.Filters/Extensions/FilterObjectFieldDescriptorExtensions.cs index 99f7d8f26ed..2cc572590db 100644 --- a/src/Core/Types.Filters/Extensions/FilterObjectFieldDescriptorExtensions.cs +++ b/src/Core/Types.Filters/Extensions/FilterObjectFieldDescriptorExtensions.cs @@ -2,23 +2,60 @@ using System.Threading.Tasks; using HotChocolate.Resolvers; using HotChocolate.Types.Descriptors; +using HotChocolate.Utilities; namespace HotChocolate.Types.Filters { public static class FilterObjectFieldDescriptorExtensions { + public static IObjectFieldDescriptor UseFilter( + this IObjectFieldDescriptor descriptor) + { + if (descriptor is null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + if (!TypeInspector.Default.TryCreate( + typeof(T), out TypeInfo typeInfo)) + { + // TODO : resources + throw new ArgumentException( + "Cannot handle the specified type.", + nameof(descriptor)); + } + + Type filterType = + typeof(FilterInputType<>).MakeGenericType(typeInfo.ClrType); + + return UseFilter(descriptor, filterType); + } + public static IObjectFieldDescriptor UseFilter( this IObjectFieldDescriptor descriptor) { - FieldMiddleware placeholder = - next => context => Task.CompletedTask; - Type middlewareDefinition = typeof(FilterMiddleware<>); + if (descriptor is null) + { + throw new ArgumentNullException(nameof(descriptor)); + } Type filterType = typeof(IFilterInputType).IsAssignableFrom(typeof(T)) ? typeof(T) : typeof(FilterInputType<>).MakeGenericType(typeof(T)); + return UseFilter(descriptor, filterType); + } + + private static TDescriptor UseFilter( + TDescriptor descriptor, + Type filterType) + where TDescriptor : IObjectFieldDescriptor + { + FieldMiddleware placeholder = + next => context => Task.CompletedTask; + Type middlewareDefinition = typeof(QueryableFilterMiddleware<>); + descriptor .AddFilterArguments(filterType) .Use(placeholder) diff --git a/src/Core/Types.Filters/FilterMiddleware.cs b/src/Core/Types.Filters/QueryableFilterMiddleware.cs similarity index 92% rename from src/Core/Types.Filters/FilterMiddleware.cs rename to src/Core/Types.Filters/QueryableFilterMiddleware.cs index 0861d660356..9e2badeabf2 100644 --- a/src/Core/Types.Filters/FilterMiddleware.cs +++ b/src/Core/Types.Filters/QueryableFilterMiddleware.cs @@ -9,12 +9,14 @@ namespace HotChocolate.Types.Filters { - public class FilterMiddleware + public class QueryableFilterMiddleware { private readonly FieldDelegate _next; private readonly ITypeConversion _converter; - public FilterMiddleware(FieldDelegate next, ITypeConversion converter) + public QueryableFilterMiddleware( + FieldDelegate next, + ITypeConversion converter) { _next = next ?? throw new ArgumentNullException(nameof(next)); _converter = converter ?? TypeConversion.Default; diff --git a/src/Core/Types.Filters/QueryableFilterVisitor.cs b/src/Core/Types.Filters/QueryableFilterVisitor.cs index ea3a1b03503..d5f56cf6e32 100644 --- a/src/Core/Types.Filters/QueryableFilterVisitor.cs +++ b/src/Core/Types.Filters/QueryableFilterVisitor.cs @@ -26,6 +26,7 @@ public QueryableFilterVisitor( _source = source ?? throw new ArgumentNullException(nameof(source)); _instance = _parameter = Expression.Parameter(source); _opHandlers = ExpressionOperationHandlers.All; + _converter = converter; Level.Push(new Queue()); Instance.Push(_instance); diff --git a/src/Core/Types/Types/Descriptors/Contracts/IObjectFieldDescriptor~1.cs b/src/Core/Types/Types/Descriptors/Contracts/IObjectFieldDescriptor~1.cs new file mode 100644 index 00000000000..9f19e0bad73 --- /dev/null +++ b/src/Core/Types/Types/Descriptors/Contracts/IObjectFieldDescriptor~1.cs @@ -0,0 +1,7 @@ +namespace HotChocolate.Types +{ + public interface IObjectFieldDescriptor + : IObjectFieldDescriptor + { + } +} diff --git a/src/Core/Types/Types/Descriptors/Contracts/IObjectTypeDescriptor~1.cs b/src/Core/Types/Types/Descriptors/Contracts/IObjectTypeDescriptor~1.cs index 26df7506213..8fcdb732c88 100644 --- a/src/Core/Types/Types/Descriptors/Contracts/IObjectTypeDescriptor~1.cs +++ b/src/Core/Types/Types/Descriptors/Contracts/IObjectTypeDescriptor~1.cs @@ -130,7 +130,7 @@ IObjectFieldDescriptor Field( /// An expression selecting a property or method of /// . /// - IObjectFieldDescriptor Field( + IObjectFieldDescriptor Field( Expression> propertyOrMethod); /// diff --git a/src/Core/Types/Types/Descriptors/ObjectFieldDescriptor.cs b/src/Core/Types/Types/Descriptors/ObjectFieldDescriptor.cs index 0a4cc3c074f..75676a16c18 100644 --- a/src/Core/Types/Types/Descriptors/ObjectFieldDescriptor.cs +++ b/src/Core/Types/Types/Descriptors/ObjectFieldDescriptor.cs @@ -13,7 +13,7 @@ public class ObjectFieldDescriptor { private bool _argumentsInitialized; - public ObjectFieldDescriptor( + protected ObjectFieldDescriptor( IDescriptorContext context, NameString fieldName) : base(context) @@ -21,14 +21,14 @@ public ObjectFieldDescriptor( Definition.Name = fieldName.EnsureNotEmpty(nameof(fieldName)); } - public ObjectFieldDescriptor( + protected ObjectFieldDescriptor( IDescriptorContext context, MemberInfo member) : this(context, member, null) { } - public ObjectFieldDescriptor( + protected ObjectFieldDescriptor( IDescriptorContext context, MemberInfo member, Type resolverType) @@ -214,5 +214,21 @@ public static ObjectFieldDescriptor New( MemberInfo member, Type resolverType) => new ObjectFieldDescriptor(context, member, resolverType); + + public static ObjectFieldDescriptor New( + IDescriptorContext context, + NameString fieldName) => + new ObjectFieldDescriptor(context, fieldName); + + public static ObjectFieldDescriptor New( + IDescriptorContext context, + MemberInfo member) => + new ObjectFieldDescriptor(context, member); + + public static ObjectFieldDescriptor New( + IDescriptorContext context, + MemberInfo member, + Type resolverType) => + new ObjectFieldDescriptor(context, member, resolverType); } } diff --git a/src/Core/Types/Types/Descriptors/ObjectFieldDescriptor~1.cs b/src/Core/Types/Types/Descriptors/ObjectFieldDescriptor~1.cs new file mode 100644 index 00000000000..80cef6985c6 --- /dev/null +++ b/src/Core/Types/Types/Descriptors/ObjectFieldDescriptor~1.cs @@ -0,0 +1,32 @@ +using System; +using System.Reflection; + +namespace HotChocolate.Types.Descriptors +{ + public class ObjectFieldDescriptor + : ObjectFieldDescriptor + , IObjectFieldDescriptor + { + internal protected ObjectFieldDescriptor( + IDescriptorContext context, + NameString fieldName) + : base(context, fieldName) + { + } + + internal protected ObjectFieldDescriptor( + IDescriptorContext context, + MemberInfo member) + : base(context, member) + { + } + + internal protected ObjectFieldDescriptor( + IDescriptorContext context, + MemberInfo member, + Type resolverType) + : base(context, member, resolverType) + { + } + } +} diff --git a/src/Core/Types/Types/Descriptors/ObjectTypeDescriptor.cs b/src/Core/Types/Types/Descriptors/ObjectTypeDescriptor.cs index 208302ffe12..6089948bb86 100644 --- a/src/Core/Types/Types/Descriptors/ObjectTypeDescriptor.cs +++ b/src/Core/Types/Types/Descriptors/ObjectTypeDescriptor.cs @@ -226,7 +226,7 @@ public IObjectTypeDescriptor IsOfType(IsOfType isOfType) public IObjectFieldDescriptor Field(NameString name) { - var fieldDescriptor = new ObjectFieldDescriptor(Context, name); + var fieldDescriptor = ObjectFieldDescriptor.New(Context, name); Fields.Add(fieldDescriptor); return fieldDescriptor; } @@ -235,8 +235,8 @@ public IObjectFieldDescriptor Field( Expression> propertyOrMethod) => Field(propertyOrMethod); - public IObjectFieldDescriptor Field( - Expression> propertyOrMethod) + public IObjectFieldDescriptor Field( + Expression> propertyOrMethod) { if (propertyOrMethod == null) { @@ -246,7 +246,7 @@ public IObjectFieldDescriptor Field( MemberInfo member = propertyOrMethod.ExtractMember(); if (member is PropertyInfo || member is MethodInfo) { - var fieldDescriptor = new ObjectFieldDescriptor( + var fieldDescriptor = ObjectFieldDescriptor.New( Context, member, typeof(TResolver)); Fields.Add(fieldDescriptor); return fieldDescriptor; diff --git a/src/Core/Types/Types/Descriptors/ObjectTypeDescriptor~1.cs b/src/Core/Types/Types/Descriptors/ObjectTypeDescriptor~1.cs index 6a51fd472b5..0542ed914a4 100644 --- a/src/Core/Types/Types/Descriptors/ObjectTypeDescriptor~1.cs +++ b/src/Core/Types/Types/Descriptors/ObjectTypeDescriptor~1.cs @@ -108,7 +108,7 @@ public IObjectFieldDescriptor Field( return base.Field(propertyOrMethod); } - public IObjectFieldDescriptor Field( + public IObjectFieldDescriptor Field( Expression> propertyOrMethod) { return base.Field(propertyOrMethod); diff --git a/src/Core/Types/Utilities/TypeInfo.cs b/src/Core/Types/Utilities/TypeInfo.cs index b9e8d51ee59..9a68bef6a75 100644 --- a/src/Core/Types/Utilities/TypeInfo.cs +++ b/src/Core/Types/Utilities/TypeInfo.cs @@ -4,7 +4,7 @@ namespace HotChocolate.Utilities { - internal sealed class TypeInfo + public sealed class TypeInfo { public TypeInfo(Type clrType, IReadOnlyList components, diff --git a/src/Core/Types/Utilities/TypeInspector.cs b/src/Core/Types/Utilities/TypeInspector.cs index b945729eb1b..8b8920b901e 100644 --- a/src/Core/Types/Utilities/TypeInspector.cs +++ b/src/Core/Types/Utilities/TypeInspector.cs @@ -1,9 +1,10 @@ -using System; +using System.Collections.Concurrent; +using System; using System.Collections.Immutable; namespace HotChocolate.Utilities { - internal class TypeInspector + public class TypeInspector : ITypeInfoFactory { private static readonly ITypeInfoFactory[] _factories = @@ -12,23 +13,19 @@ internal class TypeInspector new DotNetTypeInfoFactory() }; - private ImmutableDictionary _typeInfoCache = - ImmutableDictionary.Empty; + private readonly ConcurrentDictionary _cache = + new ConcurrentDictionary(); public bool TryCreate(Type type, out TypeInfo typeInfo) { - if (!_typeInfoCache.TryGetValue(type, out typeInfo)) + if (!_cache.TryGetValue(type, out typeInfo)) { if (!TryCreateInternal(type, out typeInfo)) { typeInfo = default; return false; } - - lock (_typeInfoCache) - { - _typeInfoCache = _typeInfoCache.SetItem(type, typeInfo); - } + _cache.TryAdd(type, typeInfo); } return true; } @@ -46,5 +43,7 @@ private bool TryCreateInternal(Type type, out TypeInfo typeInfo) typeInfo = default; return false; } + + public static TypeInspector Default { get; } = new TypeInspector(); } }