diff --git a/src/HotChocolate/Data/src/Data/DataResources.Designer.cs b/src/HotChocolate/Data/src/Data/DataResources.Designer.cs
index 02c68180dd2..152714f977f 100644
--- a/src/HotChocolate/Data/src/Data/DataResources.Designer.cs
+++ b/src/HotChocolate/Data/src/Data/DataResources.Designer.cs
@@ -489,6 +489,18 @@ internal class DataResources {
}
}
+ internal static string QueryableFiltering_ExpressionParameterInvalid {
+ get {
+ return ResourceManager.GetString("QueryableFiltering_ExpressionParameterInvalid", resourceCulture);
+ }
+ }
+
+ internal static string QueryableFilterProvider_ExpressionParameterInvalid {
+ get {
+ return ResourceManager.GetString("QueryableFilterProvider_ExpressionParameterInvalid", resourceCulture);
+ }
+ }
+
internal static string QueryableFiltering_NoMemberDeclared {
get {
return ResourceManager.GetString("QueryableFiltering_NoMemberDeclared", resourceCulture);
diff --git a/src/HotChocolate/Data/src/Data/DataResources.resx b/src/HotChocolate/Data/src/Data/DataResources.resx
index 9d570c8a817..f6be4b1f5cc 100644
--- a/src/HotChocolate/Data/src/Data/DataResources.resx
+++ b/src/HotChocolate/Data/src/Data/DataResources.resx
@@ -245,6 +245,12 @@
Filtering needs a member of type PropertyInfo or MethodInfo but received {0}! This error occured on {1}.{2}: {3}
+
+ Filtering needs a expression with exactly one parameter of type {0}! This error occured on {1}.{2}: {3}
+
+
+ Filtering needs a expression with exactly one parameter of type {0}! Check the expression on field {1}.
+
Filtering needs a member of type PropertyInfo or MethodInfo no member was found! This error occured on {0}.{1}: {2}
diff --git a/src/HotChocolate/Data/src/Data/Filters/Convention/FilterConvention.cs b/src/HotChocolate/Data/src/Data/Filters/Convention/FilterConvention.cs
index 57fcbfbe133..62cba581747 100644
--- a/src/HotChocolate/Data/src/Data/Filters/Convention/FilterConvention.cs
+++ b/src/HotChocolate/Data/src/Data/Filters/Convention/FilterConvention.cs
@@ -330,6 +330,12 @@ public bool IsOrAllowed()
return false;
}
+ public IFilterMetadata? CreateMetaData(
+ ITypeCompletionContext context,
+ IFilterInputTypeDefinition typeDefinition,
+ IFilterFieldDefinition fieldDefinition)
+ => _provider.CreateMetaData(context, typeDefinition, fieldDefinition);
+
private bool TryCreateFilterType(
IExtendedType runtimeType,
[NotNullWhen(true)] out Type? type)
diff --git a/src/HotChocolate/Data/src/Data/Filters/Convention/FilterTypeInterceptor.cs b/src/HotChocolate/Data/src/Data/Filters/Convention/FilterTypeInterceptor.cs
index 68be4b3d6c5..0f54d0141e0 100644
--- a/src/HotChocolate/Data/src/Data/Filters/Convention/FilterTypeInterceptor.cs
+++ b/src/HotChocolate/Data/src/Data/Filters/Convention/FilterTypeInterceptor.cs
@@ -204,8 +204,12 @@ TypeSystemObjectBase Factory(IDescriptorContext _)
out IFilterFieldHandler? handler))
{
filterFieldDefinition.Handler = handler;
- }
- else
+ }
+
+ filterFieldDefinition.Metadata =
+ convention.CreateMetaData(completionContext, def, filterFieldDefinition);
+
+ if (filterFieldDefinition.Handler is null)
{
throw ThrowHelper
.FilterInterceptor_NoHandlerFoundForField(def, filterFieldDefinition);
diff --git a/src/HotChocolate/Data/src/Data/Filters/Convention/IFilterConvention.cs b/src/HotChocolate/Data/src/Data/Filters/Convention/IFilterConvention.cs
index e68fcce0b9e..e469b5e1168 100644
--- a/src/HotChocolate/Data/src/Data/Filters/Convention/IFilterConvention.cs
+++ b/src/HotChocolate/Data/src/Data/Filters/Convention/IFilterConvention.cs
@@ -135,6 +135,14 @@ public interface IFilterConvention : IConvention
IFilterFieldDefinition fieldDefinition,
[NotNullWhen(true)] out IFilterFieldHandler? handler);
+ ///
+ /// Creates metadata for a field that the provider can pick up an use for the translation
+ ///
+ IFilterMetadata? CreateMetaData(
+ ITypeCompletionContext context,
+ IFilterInputTypeDefinition typeDefinition,
+ IFilterFieldDefinition fieldDefinition);
+
///
/// Creates a middleware that represents the filter execution logic
/// for the specified entity type.
diff --git a/src/HotChocolate/Data/src/Data/Filters/Expressions/ExpressionFilterMetadata.cs b/src/HotChocolate/Data/src/Data/Filters/Expressions/ExpressionFilterMetadata.cs
new file mode 100644
index 00000000000..5aab3770f5b
--- /dev/null
+++ b/src/HotChocolate/Data/src/Data/Filters/Expressions/ExpressionFilterMetadata.cs
@@ -0,0 +1,16 @@
+using System.Linq.Expressions;
+
+namespace HotChocolate.Data.Filters.Expressions;
+
+///
+/// Defines meta that for a filter field that the provider can use to build the database query
+///
+public class ExpressionFilterMetadata : IFilterMetadata
+{
+ public ExpressionFilterMetadata(Expression? expression)
+ {
+ Expression = expression;
+ }
+
+ public Expression? Expression { get; }
+}
diff --git a/src/HotChocolate/Data/src/Data/Filters/Expressions/Handlers/QueryableDefaultFieldHandler.cs b/src/HotChocolate/Data/src/Data/Filters/Expressions/Handlers/QueryableDefaultFieldHandler.cs
index 9149d20abb6..464f758a883 100644
--- a/src/HotChocolate/Data/src/Data/Filters/Expressions/Handlers/QueryableDefaultFieldHandler.cs
+++ b/src/HotChocolate/Data/src/Data/Filters/Expressions/Handlers/QueryableDefaultFieldHandler.cs
@@ -1,5 +1,6 @@
using System;
using System.Diagnostics.CodeAnalysis;
+using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using HotChocolate.Configuration;
@@ -28,7 +29,7 @@ public class QueryableDefaultFieldHandler
IFilterInputTypeDefinition typeDefinition,
IFilterFieldDefinition fieldDefinition) =>
fieldDefinition is not FilterOperationFieldDefinition &&
- fieldDefinition.Member is not null;
+ (fieldDefinition.Member is not null || fieldDefinition.Expression is not null);
public override bool TryHandleEnter(
QueryableFilterContext context,
@@ -51,20 +52,38 @@ public class QueryableDefaultFieldHandler
return false;
}
- Expression nestedProperty = field.Member switch
+ Expression nestedProperty;
+ if (field.Metadata is ExpressionFilterMetadata { Expression: LambdaExpression expression })
{
- PropertyInfo propertyInfo =>
- Expression.Property(context.GetInstance(), propertyInfo),
+ if (expression.Parameters.Count != 1 ||
+ expression.Parameters[0].Type != context.RuntimeTypes.Peek()!.Source)
+ {
+ throw ThrowHelper.QueryableFiltering_ExpressionParameterInvalid(
+ field.RuntimeType.Source,
+ field);
+ }
- MethodInfo methodInfo =>
- Expression.Call(context.GetInstance(), methodInfo),
+ nestedProperty = ReplaceVariableExpressionVisitor
+ .ReplaceParameter(expression, expression.Parameters[0], context.GetInstance())
+ .Body;
+ }
+ else
+ {
+ nestedProperty = field.Member switch
+ {
+ PropertyInfo propertyInfo =>
+ Expression.Property(context.GetInstance(), propertyInfo),
- null =>
- throw ThrowHelper.QueryableFiltering_NoMemberDeclared(field),
+ MethodInfo methodInfo =>
+ Expression.Call(context.GetInstance(), methodInfo),
- _ =>
- throw ThrowHelper.QueryableFiltering_MemberInvalid(field.Member, field)
- };
+ null =>
+ throw ThrowHelper.QueryableFiltering_NoMemberDeclared(field),
+
+ _ =>
+ throw ThrowHelper.QueryableFiltering_MemberInvalid(field.Member, field)
+ };
+ }
context.PushInstance(nestedProperty);
context.RuntimeTypes.Push(field.RuntimeType);
@@ -101,4 +120,34 @@ public class QueryableDefaultFieldHandler
action = SyntaxVisitor.Continue;
return true;
}
+
+ private sealed class ReplaceVariableExpressionVisitor : ExpressionVisitor
+ {
+ private readonly Expression _replacement;
+ private readonly ParameterExpression _parameter;
+
+ public ReplaceVariableExpressionVisitor(
+ Expression replacement,
+ ParameterExpression parameter)
+ {
+ _replacement = replacement;
+ _parameter = parameter;
+ }
+
+ protected override Expression VisitParameter(ParameterExpression node)
+ {
+ if (node == _parameter)
+ {
+ return _replacement;
+ }
+ return base.VisitParameter(node);
+ }
+
+ public static LambdaExpression ReplaceParameter(
+ LambdaExpression lambda,
+ ParameterExpression parameter,
+ Expression replacement)
+ => (LambdaExpression)
+ new ReplaceVariableExpressionVisitor(replacement, parameter).Visit(lambda);
+ }
}
diff --git a/src/HotChocolate/Data/src/Data/Filters/Expressions/QueryableFilterProvider.cs b/src/HotChocolate/Data/src/Data/Filters/Expressions/QueryableFilterProvider.cs
index 69b6e553168..9ba5122a6f0 100644
--- a/src/HotChocolate/Data/src/Data/Filters/Expressions/QueryableFilterProvider.cs
+++ b/src/HotChocolate/Data/src/Data/Filters/Expressions/QueryableFilterProvider.cs
@@ -4,6 +4,7 @@
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
+using HotChocolate.Configuration;
using HotChocolate.Language;
using HotChocolate.Resolvers;
using HotChocolate.Types;
@@ -150,4 +151,27 @@ private static ApplyFiltering CreateApplicator(NameString argumentN
contextData[ContextVisitFilterArgumentKey] = argumentKey;
contextData[ContextArgumentNameKey] = argumentName;
}
+
+ public override IFilterMetadata? CreateMetaData(
+ ITypeCompletionContext context,
+ IFilterInputTypeDefinition typeDefinition,
+ IFilterFieldDefinition fieldDefinition)
+ {
+ if (fieldDefinition.Expression is not null)
+ {
+ if (fieldDefinition.Expression is not LambdaExpression lambda ||
+ lambda.Parameters.Count != 1 ||
+ lambda.Parameters[0].Type != typeDefinition.EntityType)
+ {
+ throw ThrowHelper.QueryableFilterProvider_ExpressionParameterInvalid(
+ context.Type,
+ typeDefinition,
+ fieldDefinition);
+ }
+
+ return new ExpressionFilterMetadata(fieldDefinition.Expression);
+ }
+
+ return null;
+ }
}
diff --git a/src/HotChocolate/Data/src/Data/Filters/Fields/FilterField.cs b/src/HotChocolate/Data/src/Data/Filters/Fields/FilterField.cs
index 39fc9aafcb3..39e4701b03b 100644
--- a/src/HotChocolate/Data/src/Data/Filters/Fields/FilterField.cs
+++ b/src/HotChocolate/Data/src/Data/Filters/Fields/FilterField.cs
@@ -20,6 +20,7 @@ internal FilterField(FilterFieldDefinition definition, int index)
{
Member = definition.Member;
Handler = definition.Handler!;
+ Metadata = definition.Metadata;
}
public new FilterInputType DeclaringType => (FilterInputType)base.DeclaringType;
@@ -32,6 +33,8 @@ internal FilterField(FilterFieldDefinition definition, int index)
public IFilterFieldHandler Handler { get; }
+ public IFilterMetadata? Metadata { get; }
+
protected override void OnCompleteField(
ITypeCompletionContext context,
ITypeSystemMember declaringMember,
@@ -43,5 +46,9 @@ internal FilterField(FilterFieldDefinition definition, int index)
{
RuntimeType = context.TypeInspector.GetReturnType(Member, ignoreAttributes: true);
}
+ else if (base.RuntimeType is { } runtimeType)
+ {
+ RuntimeType = context.TypeInspector.GetType(runtimeType);
+ }
}
}
diff --git a/src/HotChocolate/Data/src/Data/Filters/Fields/IFilterField.cs b/src/HotChocolate/Data/src/Data/Filters/Fields/IFilterField.cs
index a97a515a8d2..397b34069b7 100644
--- a/src/HotChocolate/Data/src/Data/Filters/Fields/IFilterField.cs
+++ b/src/HotChocolate/Data/src/Data/Filters/Fields/IFilterField.cs
@@ -16,4 +16,6 @@ public interface IFilterField : IInputField
new IExtendedType? RuntimeType { get; }
IFilterFieldHandler Handler { get; }
+
+ IFilterMetadata? Metadata { get; }
}
diff --git a/src/HotChocolate/Data/src/Data/Filters/FilterFieldDefinition.cs b/src/HotChocolate/Data/src/Data/Filters/FilterFieldDefinition.cs
index 52357629853..4deea788e5c 100644
--- a/src/HotChocolate/Data/src/Data/Filters/FilterFieldDefinition.cs
+++ b/src/HotChocolate/Data/src/Data/Filters/FilterFieldDefinition.cs
@@ -1,6 +1,8 @@
+using System.Linq.Expressions;
using System;
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Reflection;
+using HotChocolate.Internal;
using HotChocolate.Types;
using HotChocolate.Types.Descriptors;
using HotChocolate.Types.Descriptors.Definitions;
@@ -18,7 +20,11 @@ public class FilterFieldDefinition
public IFilterFieldHandler? Handler { get; set; }
- public string? Scope { get; set; }
+ public Expression? Expression { get; set; }
+
+ internal IFilterMetadata? Metadata { get; set; }
+
+ public string? Scope { get; set; }
public List AllowedOperations => _allowedOperations ??= new List();
@@ -65,5 +71,5 @@ internal void MergeInto(FilterFieldDefinition target)
{
target.CreateFieldTypeDefinition = CreateFieldTypeDefinition;
}
- }
+ }
}
diff --git a/src/HotChocolate/Data/src/Data/Filters/FilterFieldDescriptor.cs b/src/HotChocolate/Data/src/Data/Filters/FilterFieldDescriptor.cs
index 12f7317f6f7..70a7ee21a19 100644
--- a/src/HotChocolate/Data/src/Data/Filters/FilterFieldDescriptor.cs
+++ b/src/HotChocolate/Data/src/Data/Filters/FilterFieldDescriptor.cs
@@ -1,4 +1,5 @@
using System;
+using System.Linq.Expressions;
using System.Reflection;
using HotChocolate.Language;
using HotChocolate.Types;
@@ -38,6 +39,23 @@ public class FilterFieldDescriptor
Definition.Scope = scope;
}
+ protected FilterFieldDescriptor(
+ IDescriptorContext context,
+ string? scope,
+ Expression expression)
+ : base(context)
+ {
+ IFilterConvention convention = context.GetFilterConvention(scope);
+
+ Definition.Expression = expression;
+ Definition.Scope = scope;
+ if (Definition.Expression is LambdaExpression lambda)
+ {
+ Definition.Type = convention.GetFieldType(lambda.ReturnType);
+ Definition.RuntimeType = lambda.ReturnType;
+ }
+ }
+
protected internal FilterFieldDescriptor(
IDescriptorContext context,
string? scope)
@@ -171,4 +189,10 @@ public new IFilterFieldDescriptor Directive()
NameString fieldName,
string? scope) =>
new FilterFieldDescriptor(context, scope, fieldName);
+
+ internal static FilterFieldDescriptor New(
+ IDescriptorContext context,
+ string? scope,
+ Expression expression) =>
+ new FilterFieldDescriptor(context, scope, expression);
}
diff --git a/src/HotChocolate/Data/src/Data/Filters/FilterInputTypeDescriptor`1.cs b/src/HotChocolate/Data/src/Data/Filters/FilterInputTypeDescriptor`1.cs
index bea6d4d4be8..a4cded82f09 100644
--- a/src/HotChocolate/Data/src/Data/Filters/FilterInputTypeDescriptor`1.cs
+++ b/src/HotChocolate/Data/src/Data/Filters/FilterInputTypeDescriptor`1.cs
@@ -99,23 +99,31 @@ public new IFilterInputTypeDescriptor BindFieldsImplicitly()
///
public IFilterFieldDescriptor Field(Expression> propertyOrMember)
{
- if (propertyOrMember.ExtractMember() is PropertyInfo m)
+ switch (propertyOrMember.TryExtractMember())
{
- FilterFieldDescriptor? fieldDescriptor =
- Fields.FirstOrDefault(t => t.Definition.Member == m);
-
- if (fieldDescriptor is null)
- {
- fieldDescriptor = FilterFieldDescriptor.New(Context, Definition.Scope, m);
+ case PropertyInfo m:
+ FilterFieldDescriptor? fieldDescriptor =
+ Fields.FirstOrDefault(t => t.Definition.Member == m);
+
+ if (fieldDescriptor is null)
+ {
+ fieldDescriptor = FilterFieldDescriptor.New(Context, Definition.Scope, m);
+ Fields.Add(fieldDescriptor);
+ }
+
+ return fieldDescriptor;
+
+ case MethodInfo m:
+ throw new ArgumentException(
+ FilterInputTypeDescriptor_Field_OnlyProperties,
+ nameof(propertyOrMember));
+
+ default:
+ fieldDescriptor = FilterFieldDescriptor
+ .New(Context, Definition.Scope, propertyOrMember);
Fields.Add(fieldDescriptor);
- }
-
- return fieldDescriptor;
+ return fieldDescriptor;
}
-
- throw new ArgumentException(
- FilterInputTypeDescriptor_Field_OnlyProperties,
- nameof(propertyOrMember));
}
///
diff --git a/src/HotChocolate/Data/src/Data/Filters/IFilterFieldDefinition.cs b/src/HotChocolate/Data/src/Data/Filters/IFilterFieldDefinition.cs
index 33771edd42e..cbe5baa51f8 100644
--- a/src/HotChocolate/Data/src/Data/Filters/IFilterFieldDefinition.cs
+++ b/src/HotChocolate/Data/src/Data/Filters/IFilterFieldDefinition.cs
@@ -1,14 +1,23 @@
+using System.Linq.Expressions;
using System.Collections.Generic;
using System.Reflection;
+using HotChocolate.Types;
+using HotChocolate.Types.Descriptors.Definitions;
namespace HotChocolate.Data.Filters;
public interface IFilterFieldDefinition
+ : IDefinition
+ , IHasDirectiveDefinition
+ , IHasIgnore
+ , IHasScope
{
MemberInfo? Member { get; }
IFilterFieldHandler? Handler { get; }
+ Expression? Expression { get; }
+
string? Scope { get; }
List AllowedOperations { get; }
diff --git a/src/HotChocolate/Data/src/Data/Filters/IFilterMetaData.cs b/src/HotChocolate/Data/src/Data/Filters/IFilterMetaData.cs
new file mode 100644
index 00000000000..8cf505fe6bf
--- /dev/null
+++ b/src/HotChocolate/Data/src/Data/Filters/IFilterMetaData.cs
@@ -0,0 +1,10 @@
+namespace HotChocolate.Data.Filters;
+
+///
+/// Defines meta that for a filter field that the provider can use to build the database query
+///
+///
+#pragma warning disable CA1040 // Avoid empty interfaces
+public interface IFilterMetadata
+{
+}
diff --git a/src/HotChocolate/Data/src/Data/Filters/Visitor/FilterProvider.cs b/src/HotChocolate/Data/src/Data/Filters/Visitor/FilterProvider.cs
index 96531342af5..115b441a785 100644
--- a/src/HotChocolate/Data/src/Data/Filters/Visitor/FilterProvider.cs
+++ b/src/HotChocolate/Data/src/Data/Filters/Visitor/FilterProvider.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using HotChocolate.Configuration;
using HotChocolate.Resolvers;
using HotChocolate.Types;
using HotChocolate.Types.Descriptors;
@@ -152,4 +153,10 @@ protected internal override void Complete(IConventionContext context)
IObjectFieldDescriptor descriptor)
{
}
+
+ public virtual IFilterMetadata? CreateMetaData(
+ ITypeCompletionContext context,
+ IFilterInputTypeDefinition typeDefinition,
+ IFilterFieldDefinition fieldDefinition)
+ => null;
}
diff --git a/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterProvider.cs b/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterProvider.cs
index 9ffe5e4b9b0..abbd6f3f482 100644
--- a/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterProvider.cs
+++ b/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterProvider.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using HotChocolate.Configuration;
using HotChocolate.Resolvers;
using HotChocolate.Types;
@@ -33,4 +34,12 @@ public interface IFilterProvider
/// data to the field.
///
void ConfigureField(NameString argumentName, IObjectFieldDescriptor descriptor);
+
+ ///
+ /// Creates metadata for a field that the provider can pick up an use for the translation
+ ///
+ IFilterMetadata? CreateMetaData(
+ ITypeCompletionContext context,
+ IFilterInputTypeDefinition typeDefinition,
+ IFilterFieldDefinition fieldDefinition);
}
diff --git a/src/HotChocolate/Data/src/Data/HotChocolate.Data.csproj b/src/HotChocolate/Data/src/Data/HotChocolate.Data.csproj
index 5950987d6f6..dcc45c5d7c6 100644
--- a/src/HotChocolate/Data/src/Data/HotChocolate.Data.csproj
+++ b/src/HotChocolate/Data/src/Data/HotChocolate.Data.csproj
@@ -29,6 +29,11 @@
+
+
+
+
+
True
diff --git a/src/HotChocolate/Data/src/Data/ThrowHelper.cs b/src/HotChocolate/Data/src/Data/ThrowHelper.cs
index 5bdc2b42bb6..facff875290 100644
--- a/src/HotChocolate/Data/src/Data/ThrowHelper.cs
+++ b/src/HotChocolate/Data/src/Data/ThrowHelper.cs
@@ -449,6 +449,30 @@ internal static class ThrowHelper
field.Name,
field.Type.Print()));
+ public static InvalidOperationException QueryableFiltering_ExpressionParameterInvalid(
+ Type type,
+ IFilterField field) =>
+ new(string.Format(
+ CultureInfo.CurrentCulture,
+ DataResources.QueryableFiltering_ExpressionParameterInvalid,
+ type.FullName,
+ field.DeclaringType.Print(),
+ field.Name,
+ field.Type.Print()));
+
+ public static SchemaException QueryableFilterProvider_ExpressionParameterInvalid(
+ ITypeSystemObject type,
+ IFilterInputTypeDefinition typeDefinition,
+ IFilterFieldDefinition field) =>
+ new(SchemaErrorBuilder
+ .New()
+ .SetMessage(
+ DataResources.QueryableFilterProvider_ExpressionParameterInvalid,
+ typeDefinition.EntityType?.FullName,
+ field.Name)
+ .SetTypeSystemObject(type)
+ .Build());
+
public static InvalidOperationException QueryableFiltering_NoMemberDeclared(IFilterField field)
=>
new(string.Format(
diff --git a/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/QueryableFilterVisitorComparableTests.cs b/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/QueryableFilterVisitorComparableTests.cs
index 7ae896bfc15..658b387987a 100644
--- a/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/QueryableFilterVisitorComparableTests.cs
+++ b/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/QueryableFilterVisitorComparableTests.cs
@@ -160,7 +160,6 @@ public async Task Create_ShortNotGreaterThan_Expression()
res4.MatchSnapshot("null");
}
-
[Fact]
public async Task Create_ShortGreaterThanOrEquals_Expression()
{
diff --git a/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/QueryableFilterVisitorExpressionTests.cs b/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/QueryableFilterVisitorExpressionTests.cs
new file mode 100644
index 00000000000..e860e0c0a40
--- /dev/null
+++ b/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/QueryableFilterVisitorExpressionTests.cs
@@ -0,0 +1,141 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using HotChocolate.Execution;
+using HotChocolate.Execution.Configuration;
+using HotChocolate.Tests;
+using HotChocolate.Types;
+using Microsoft.Extensions.DependencyInjection;
+using Snapshooter.Xunit;
+using Xunit;
+
+namespace HotChocolate.Data.Filters;
+
+public class QueryableFilterVisitorExpressionTests : IClassFixture
+{
+ private static readonly Foo[] _fooEntities =
+ {
+ new Foo { Name = "Foo", LastName = "Galoo", Bars = new Bar[]{ new Bar { Value="A"} } },
+ new Foo { Name = "Sam", LastName = "Sampleman", Bars = Array.Empty() }
+ };
+
+ private readonly SchemaCache _cache;
+
+ public QueryableFilterVisitorExpressionTests(SchemaCache cache)
+ {
+ _cache = cache;
+ }
+
+ [Fact]
+ public async Task Create_StringConcatExpression()
+ {
+ // arrange
+ IRequestExecutor? tester = _cache.CreateSchema(_fooEntities);
+
+ // act
+ // assert
+ IExecutionResult? res1 = await tester.ExecuteAsync(
+ QueryRequestBuilder.New()
+ .SetQuery("{ root(where: { displayName: { eq: \"Sam Sampleman\"}}){ name lastName}}")
+ .Create());
+
+ res1.MatchSnapshot("Sam_Sampleman");
+
+ IExecutionResult? res2 = await tester.ExecuteAsync(
+ QueryRequestBuilder.New()
+ .SetQuery("{ root(where: { displayName: { eq: \"NoMatch\"}}){ name lastName}}")
+ .Create());
+
+ res2.MatchSnapshot("NoMatch");
+
+ IExecutionResult? res3 = await tester.ExecuteAsync(
+ QueryRequestBuilder.New()
+ .SetQuery("{ root(where: { displayName: { eq: null}}){ name lastName}}")
+ .Create());
+
+ res3.MatchSnapshot("null");
+ }
+
+ [Fact]
+ public async Task Expression_WithMoreThanOneParameter_ThrowsException()
+ {
+ // arrange
+ IRequestExecutorBuilder builder = new ServiceCollection()
+ .AddGraphQL()
+ .AddQueryType(x => x
+ .Name("Query")
+ .Field("Foo")
+ .Resolve(Array.Empty())
+ .UseFiltering())
+ .AddType(new FilterInputType(x => x
+ .Field(x => x.LastName)
+ .Extend()
+ .OnBeforeCreate(x => x.Expression = (Foo x, string bar) => x.LastName == bar)))
+ .AddFiltering();
+
+ // act
+ async Task Call() => await builder.BuildRequestExecutorAsync();
+
+ // assert
+ SchemaException ex = await Assert.ThrowsAsync(Call);
+ ex.Errors.Single().Message.MatchSnapshot();
+ }
+
+ [Fact]
+ public async Task Create_CollectionLengthExpression()
+ {
+ // arrange
+ IRequestExecutor? tester = _cache.CreateSchema(_fooEntities);
+
+ // act
+ // assert
+ IExecutionResult? res1 = await tester.ExecuteAsync(
+ QueryRequestBuilder.New()
+ .SetQuery("{ root(where: { barLength: { eq: 1}}){ name lastName}}")
+ .Create());
+
+ res1.MatchSnapshot("1");
+
+ IExecutionResult? res2 = await tester.ExecuteAsync(
+ QueryRequestBuilder.New()
+ .SetQuery("{ root(where: { barLength: { eq: 0}}){ name lastName}}")
+ .Create());
+
+ res2.MatchSnapshot("0");
+
+ IExecutionResult? res3 = await tester.ExecuteAsync(
+ QueryRequestBuilder.New()
+ .SetQuery("{ root(where: { barLength: { eq: null}}){ name lastName}}")
+ .Create());
+
+ res3.MatchSnapshot("null");
+ }
+
+ public class Foo
+ {
+ public int Id { get; set; }
+
+ public string? Name { get; set; }
+
+ public string? LastName { get; set; }
+
+ public ICollection? Bars { get; set; }
+ }
+
+ public class Bar
+ {
+ public int Id { get; set; }
+
+ public string? Value { get; set; }
+ }
+
+ public class FooFilterInputType : FilterInputType
+ {
+ protected override void Configure(IFilterInputTypeDescriptor descriptor)
+ {
+ descriptor.Field(x => x.Name + " " + x.LastName).Name("displayName");
+ descriptor.Field(x => x.Bars!.Count).Name("barLength");
+ }
+ }
+}
diff --git a/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_CollectionLengthExpression_0.snap b/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_CollectionLengthExpression_0.snap
new file mode 100644
index 00000000000..51e534ab0a6
--- /dev/null
+++ b/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_CollectionLengthExpression_0.snap
@@ -0,0 +1,10 @@
+{
+ "data": {
+ "root": [
+ {
+ "name": "Sam",
+ "lastName": "Sampleman"
+ }
+ ]
+ }
+}
diff --git a/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_CollectionLengthExpression_1.snap b/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_CollectionLengthExpression_1.snap
new file mode 100644
index 00000000000..879f6888343
--- /dev/null
+++ b/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_CollectionLengthExpression_1.snap
@@ -0,0 +1,10 @@
+{
+ "data": {
+ "root": [
+ {
+ "name": "Foo",
+ "lastName": "Galoo"
+ }
+ ]
+ }
+}
diff --git a/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_CollectionLengthExpression_null.snap b/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_CollectionLengthExpression_null.snap
new file mode 100644
index 00000000000..d3e5801b9f1
--- /dev/null
+++ b/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_CollectionLengthExpression_null.snap
@@ -0,0 +1,24 @@
+{
+ "errors": [
+ {
+ "message": "The provided value for filter \u0060eq\u0060 of type ComparableInt32OperationFilterInput is invalid. Null values are not supported.",
+ "locations": [
+ {
+ "line": 1,
+ "column": 34
+ }
+ ],
+ "path": [
+ "root"
+ ],
+ "extensions": {
+ "code": "HC0026",
+ "expectedType": "Int!",
+ "filterType": "ComparableInt32OperationFilterInput"
+ }
+ }
+ ],
+ "data": {
+ "root": []
+ }
+}
diff --git a/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_StringConcatExpression_NoMatch.snap b/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_StringConcatExpression_NoMatch.snap
new file mode 100644
index 00000000000..c3908564f27
--- /dev/null
+++ b/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_StringConcatExpression_NoMatch.snap
@@ -0,0 +1,5 @@
+{
+ "data": {
+ "root": []
+ }
+}
diff --git a/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_StringConcatExpression_Sam_Sampleman.snap b/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_StringConcatExpression_Sam_Sampleman.snap
new file mode 100644
index 00000000000..51e534ab0a6
--- /dev/null
+++ b/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_StringConcatExpression_Sam_Sampleman.snap
@@ -0,0 +1,10 @@
+{
+ "data": {
+ "root": [
+ {
+ "name": "Sam",
+ "lastName": "Sampleman"
+ }
+ ]
+ }
+}
diff --git a/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_StringConcatExpression_null.snap b/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_StringConcatExpression_null.snap
new file mode 100644
index 00000000000..c3908564f27
--- /dev/null
+++ b/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_StringConcatExpression_null.snap
@@ -0,0 +1,5 @@
+{
+ "data": {
+ "root": []
+ }
+}
diff --git a/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Expression_WithMoreThanOneParameter_ThrowsException.snap b/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Expression_WithMoreThanOneParameter_ThrowsException.snap
new file mode 100644
index 00000000000..4811b7f8b49
--- /dev/null
+++ b/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Expression_WithMoreThanOneParameter_ThrowsException.snap
@@ -0,0 +1 @@
+Filtering needs a expression with exactly one parameter of type HotChocolate.Data.Filters.QueryableFilterVisitorExpressionTests+Foo! Check the expression on field lastName.
diff --git a/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/QueryableFilterVisitorExpressionTests.cs b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/QueryableFilterVisitorExpressionTests.cs
new file mode 100644
index 00000000000..912ad39da43
--- /dev/null
+++ b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/QueryableFilterVisitorExpressionTests.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using HotChocolate.Execution;
+using Xunit;
+
+namespace HotChocolate.Data.Filters;
+
+public class QueryableFilterVisitorExpressionTests : IClassFixture
+{
+ private static readonly Foo[] _fooEntities =
+ {
+ new Foo { Name = "Foo", LastName = "Galoo", Bars = new Bar[]{ new Bar { Value="A"} } },
+ new Foo { Name = "Sam", LastName = "Sampleman", Bars = Array.Empty() }
+ };
+
+ private readonly SchemaCache _cache;
+
+ public QueryableFilterVisitorExpressionTests(SchemaCache cache)
+ {
+ _cache = cache;
+ }
+
+ [Fact]
+ public async Task Create_StringConcatExpression()
+ {
+ // arrange
+ IRequestExecutor? tester = _cache.CreateSchema(_fooEntities);
+
+ // act
+ // assert
+ IExecutionResult? res1 = await tester.ExecuteAsync(
+ QueryRequestBuilder.New()
+ .SetQuery("{ root(where: { displayName: { eq: \"Sam Sampleman\"}}){ name lastName}}")
+ .Create());
+
+ res1.MatchSqlSnapshot("Sam_Sampleman");
+
+ IExecutionResult? res2 = await tester.ExecuteAsync(
+ QueryRequestBuilder.New()
+ .SetQuery("{ root(where: { displayName: { eq: \"NoMatch\"}}){ name lastName}}")
+ .Create());
+
+ res2.MatchSqlSnapshot("NoMatch");
+
+ IExecutionResult? res3 = await tester.ExecuteAsync(
+ QueryRequestBuilder.New()
+ .SetQuery("{ root(where: { displayName: { eq: null}}){ name lastName}}")
+ .Create());
+
+ res3.MatchSqlSnapshot("null");
+ }
+
+ [Fact]
+ public async Task Create_CollectionLengthExpression()
+ {
+ // arrange
+ IRequestExecutor? tester = _cache.CreateSchema(_fooEntities);
+
+ // act
+ // assert
+ IExecutionResult? res1 = await tester.ExecuteAsync(
+ QueryRequestBuilder.New()
+ .SetQuery("{ root(where: { barLength: { eq: 1}}){ name lastName}}")
+ .Create());
+
+ res1.MatchSqlSnapshot("1");
+
+ IExecutionResult? res2 = await tester.ExecuteAsync(
+ QueryRequestBuilder.New()
+ .SetQuery("{ root(where: { barLength: { eq: 0}}){ name lastName}}")
+ .Create());
+
+ res2.MatchSqlSnapshot("0");
+
+ IExecutionResult? res3 = await tester.ExecuteAsync(
+ QueryRequestBuilder.New()
+ .SetQuery("{ root(where: { barLength: { eq: null}}){ name lastName}}")
+ .Create());
+
+ res3.MatchSqlSnapshot("null");
+ }
+
+ public class Foo
+ {
+ public int Id { get; set; }
+
+ public string? Name { get; set; }
+
+ public string? LastName { get; set; }
+
+ public ICollection? Bars { get; set; }
+ }
+
+ public class Bar
+ {
+ public int Id { get; set; }
+
+ public string? Value { get; set; }
+ }
+
+ public class FooFilterInputType : FilterInputType
+ {
+ protected override void Configure(IFilterInputTypeDescriptor descriptor)
+ {
+ descriptor.Field(x => x.Name + " " + x.LastName).Name("displayName");
+ descriptor.Field(x => x.Bars!.Count).Name("barLength");
+ }
+ }
+}
diff --git a/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_CollectionLengthExpression_0_NET6_0.snap b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_CollectionLengthExpression_0_NET6_0.snap
new file mode 100644
index 00000000000..51e534ab0a6
--- /dev/null
+++ b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_CollectionLengthExpression_0_NET6_0.snap
@@ -0,0 +1,10 @@
+{
+ "data": {
+ "root": [
+ {
+ "name": "Sam",
+ "lastName": "Sampleman"
+ }
+ ]
+ }
+}
diff --git a/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_CollectionLengthExpression_0_sql_NET6_0.snap b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_CollectionLengthExpression_0_sql_NET6_0.snap
new file mode 100644
index 00000000000..f9bd15543de
--- /dev/null
+++ b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_CollectionLengthExpression_0_sql_NET6_0.snap
@@ -0,0 +1,6 @@
+SELECT "d"."Id", "d"."LastName", "d"."Name"
+FROM "Data" AS "d"
+WHERE (
+ SELECT COUNT(*)
+ FROM "Bar" AS "b"
+ WHERE ("d"."Id" <> NULL) AND ("d"."Id" = "b"."FooId")) = @__p_0
diff --git a/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_CollectionLengthExpression_1_NET6_0.snap b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_CollectionLengthExpression_1_NET6_0.snap
new file mode 100644
index 00000000000..879f6888343
--- /dev/null
+++ b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_CollectionLengthExpression_1_NET6_0.snap
@@ -0,0 +1,10 @@
+{
+ "data": {
+ "root": [
+ {
+ "name": "Foo",
+ "lastName": "Galoo"
+ }
+ ]
+ }
+}
diff --git a/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_CollectionLengthExpression_1_sql_NET6_0.snap b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_CollectionLengthExpression_1_sql_NET6_0.snap
new file mode 100644
index 00000000000..f9bd15543de
--- /dev/null
+++ b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_CollectionLengthExpression_1_sql_NET6_0.snap
@@ -0,0 +1,6 @@
+SELECT "d"."Id", "d"."LastName", "d"."Name"
+FROM "Data" AS "d"
+WHERE (
+ SELECT COUNT(*)
+ FROM "Bar" AS "b"
+ WHERE ("d"."Id" <> NULL) AND ("d"."Id" = "b"."FooId")) = @__p_0
diff --git a/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_CollectionLengthExpression_null_NET6_0.snap b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_CollectionLengthExpression_null_NET6_0.snap
new file mode 100644
index 00000000000..d3e5801b9f1
--- /dev/null
+++ b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_CollectionLengthExpression_null_NET6_0.snap
@@ -0,0 +1,24 @@
+{
+ "errors": [
+ {
+ "message": "The provided value for filter \u0060eq\u0060 of type ComparableInt32OperationFilterInput is invalid. Null values are not supported.",
+ "locations": [
+ {
+ "line": 1,
+ "column": 34
+ }
+ ],
+ "path": [
+ "root"
+ ],
+ "extensions": {
+ "code": "HC0026",
+ "expectedType": "Int!",
+ "filterType": "ComparableInt32OperationFilterInput"
+ }
+ }
+ ],
+ "data": {
+ "root": []
+ }
+}
diff --git a/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_StringConcatExpression_NoMatch_NET6_0.snap b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_StringConcatExpression_NoMatch_NET6_0.snap
new file mode 100644
index 00000000000..c3908564f27
--- /dev/null
+++ b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_StringConcatExpression_NoMatch_NET6_0.snap
@@ -0,0 +1,5 @@
+{
+ "data": {
+ "root": []
+ }
+}
diff --git a/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_StringConcatExpression_NoMatch_sql_NET6_0.snap b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_StringConcatExpression_NoMatch_sql_NET6_0.snap
new file mode 100644
index 00000000000..97372ebb8a2
--- /dev/null
+++ b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_StringConcatExpression_NoMatch_sql_NET6_0.snap
@@ -0,0 +1,3 @@
+SELECT "d"."Id", "d"."LastName", "d"."Name"
+FROM "Data" AS "d"
+WHERE (("d"."Name" || ' ') || "d"."LastName") = @__p_0
diff --git a/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_StringConcatExpression_Sam_Sampleman_NET6_0.snap b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_StringConcatExpression_Sam_Sampleman_NET6_0.snap
new file mode 100644
index 00000000000..51e534ab0a6
--- /dev/null
+++ b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_StringConcatExpression_Sam_Sampleman_NET6_0.snap
@@ -0,0 +1,10 @@
+{
+ "data": {
+ "root": [
+ {
+ "name": "Sam",
+ "lastName": "Sampleman"
+ }
+ ]
+ }
+}
diff --git a/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_StringConcatExpression_Sam_Sampleman_sql_NET6_0.snap b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_StringConcatExpression_Sam_Sampleman_sql_NET6_0.snap
new file mode 100644
index 00000000000..97372ebb8a2
--- /dev/null
+++ b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_StringConcatExpression_Sam_Sampleman_sql_NET6_0.snap
@@ -0,0 +1,3 @@
+SELECT "d"."Id", "d"."LastName", "d"."Name"
+FROM "Data" AS "d"
+WHERE (("d"."Name" || ' ') || "d"."LastName") = @__p_0
diff --git a/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_StringConcatExpression_null_NET6_0.snap b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_StringConcatExpression_null_NET6_0.snap
new file mode 100644
index 00000000000..c3908564f27
--- /dev/null
+++ b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_StringConcatExpression_null_NET6_0.snap
@@ -0,0 +1,5 @@
+{
+ "data": {
+ "root": []
+ }
+}
diff --git a/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_StringConcatExpression_null_sql_NET6_0.snap b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_StringConcatExpression_null_sql_NET6_0.snap
new file mode 100644
index 00000000000..97372ebb8a2
--- /dev/null
+++ b/src/HotChocolate/Data/test/Data.Filters.SqlServer.Tests/__snapshots__/QueryableFilterVisitorExpressionTests.Create_StringConcatExpression_null_sql_NET6_0.snap
@@ -0,0 +1,3 @@
+SELECT "d"."Id", "d"."LastName", "d"."Name"
+FROM "Data" AS "d"
+WHERE (("d"."Name" || ' ') || "d"."LastName") = @__p_0
diff --git a/src/HotChocolate/Data/test/Data.Filters.Tests/FilterConventionExtensionsTests.cs b/src/HotChocolate/Data/test/Data.Filters.Tests/FilterConventionExtensionsTests.cs
index f25c4eafac4..bed79adcffb 100644
--- a/src/HotChocolate/Data/test/Data.Filters.Tests/FilterConventionExtensionsTests.cs
+++ b/src/HotChocolate/Data/test/Data.Filters.Tests/FilterConventionExtensionsTests.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using HotChocolate.Configuration;
using HotChocolate.Data.Filters;
using HotChocolate.Data.Filters.Expressions;
using HotChocolate.Resolvers;
@@ -269,6 +270,11 @@ public void ConfigureField(NameString argumentName, IObjectFieldDescriptor descr
{
throw new NotImplementedException();
}
+
+ public IFilterMetadata? CreateMetaData(ITypeCompletionContext context, IFilterInputTypeDefinition typeDefinition, IFilterFieldDefinition fieldDefinition)
+ {
+ return null;
+ }
}
private sealed class MockFilterConvention : FilterConvention