diff --git a/QueryKit.IntegrationTests/Tests/HasConversionTests.cs b/QueryKit.IntegrationTests/Tests/HasConversionTests.cs index e8f1661..24b8eba 100644 --- a/QueryKit.IntegrationTests/Tests/HasConversionTests.cs +++ b/QueryKit.IntegrationTests/Tests/HasConversionTests.cs @@ -392,4 +392,36 @@ public async Task can_filter_guid_with_contains_and_has_conversion() people.Count.Should().Be(1); people[0].Id.Should().Be(person.Id); } -} \ No newline at end of file + + [Theory] + [InlineData("^^")] + [InlineData("!^^")] + public async Task can_filter_by_email_with_in_operators_and_has_conversion(string op) + { + // Arrange + var testingServiceScope = new TestingServiceScope(); + var emailOne = $"{Guid.NewGuid()}@example.com"; + var emailTwo = $"{Guid.NewGuid()}@example.com"; + var personOne = new FakeTestingPersonBuilder().WithEmail(emailOne).Build(); + var personTwo = new FakeTestingPersonBuilder().WithEmail(emailTwo).Build(); + var personThree = new FakeTestingPersonBuilder().Build(); + + await testingServiceScope.InsertAsync(personOne, personTwo, personThree); + + var input = $"""Email {op} ["{emailOne}", "{emailTwo}"]"""; + var config = new QueryKitConfiguration(config => + { + config.Property(x => x.Email).HasConversion(); + }); + var ids = new[] { personOne.Id, personTwo.Id, personThree.Id }; + + // Act + var queryablePeople = testingServiceScope.DbContext().People; + var appliedQueryable = queryablePeople.ApplyQueryKitFilter(input, config).Where(x => ids.Contains(x.Id)); + var people = await appliedQueryable.ToListAsync(); + + // Assert + var expectedIds = op == "^^" ? new[] { personOne.Id, personTwo.Id } : new[] { personThree.Id }; + people.Select(x => x.Id).Should().BeEquivalentTo(expectedIds); + } +} diff --git a/QueryKit.UnitTests/HasConversionTests.cs b/QueryKit.UnitTests/HasConversionTests.cs index 35716ef..9c4975a 100644 --- a/QueryKit.UnitTests/HasConversionTests.cs +++ b/QueryKit.UnitTests/HasConversionTests.cs @@ -453,6 +453,128 @@ public void guid_with_has_conversion_compares_a_guid_constant(string queryName) filter.ToDisplayString().Should().Be($"x => (x.Id == {KnownGuid})"); } + [Theory] + [InlineData("""Id ^^ ["2","3"]""", new[] { "two" })] + [InlineData("""Id !^^ ["2","3"]""", new[] { "one" })] + [InlineData("""Id ^^ [2, 3]""", new[] { "two" })] + public void struct_with_has_conversion_reads_a_list_for_the_in_operators(string input, string[] expectedNames) + { + // Arrange + var config = new QueryKitConfiguration(config => + { + config.Property(x => x.Id).HasConversion(); + }); + + // Act + var result = WrappedIdRows().ApplyQueryKitFilter(input, config).ToList(); + + // Assert + result.Select(x => x.Name).Should().BeEquivalentTo(expectedNames); + } + + [Fact] + public void struct_with_query_name_and_has_conversion_reads_a_list_for_the_in_operator() + { + // Arrange + var config = new QueryKitConfiguration(config => + { + config.Property(x => x.Id).HasQueryName("wrappedid").HasConversion(); + }); + + // Act + var result = WrappedIdRows().ApplyQueryKitFilter("""wrappedid ^^ ["1","2"]""", config).ToList(); + + // Assert + result.Select(x => x.Name).Should().BeEquivalentTo("one", "two"); + } + + [Fact] + public void nullable_struct_with_has_conversion_reads_a_list_for_the_in_operator() + { + // Arrange + var rows = new List + { + new() { Id = new WrappedId(1) }, + new() { Id = new WrappedId(2) }, + new() { Id = null } + }; + var config = new QueryKitConfiguration(config => + { + config.Property(x => x.Id!).HasConversion(); + }); + + // Act + var result = rows.ApplyQueryKitFilter("""Id ^^ ["2","3"]""", config).ToList(); + + // Assert + result.Select(x => x.Id).Should().BeEquivalentTo(new WrappedId?[] { new WrappedId(2) }); + } + + [Theory] + [InlineData("""Email ^^ ["b@x.com","c@x.com"]""", new[] { "b@x.com" })] + [InlineData("""Email ^^ ["a,b@x.com"]""", new[] { "a,b@x.com" })] + [InlineData("""Email ^^ ["null"]""", new[] { "null" })] + [InlineData(""""Email ^^ [""" "q" """]"""", new[] { "\"q\"" })] + public void reference_type_with_has_conversion_reads_each_list_item_as_text(string input, string[] expectedEmails) + { + // Arrange + var rows = EmailRows(); + rows.Add(new EmailRow { Email = new EmailAddressRecord("a,b@x.com") }); + rows.Add(new EmailRow { Email = new EmailAddressRecord("null") }); + rows.Add(new EmailRow { Email = new EmailAddressRecord("\"q\"") }); + rows.Add(new EmailRow { Email = new EmailAddressRecord("q") }); + rows.Add(new EmailRow { Email = null }); + var config = new QueryKitConfiguration(config => + { + config.Property(x => x.Email!).HasConversion(); + }); + + // Act + var result = rows.ApplyQueryKitFilter(input, config).ToList(); + + // Assert + result.Select(x => x.Email?.Value).Should().BeEquivalentTo(expectedEmails); + } + + [Theory] + [InlineData("""Email == "[a]" """)] + [InlineData("""Email == [a]""")] + public void text_in_brackets_stays_one_value_for_equals_with_has_conversion(string input) + { + // Arrange + var rows = EmailRows(); + rows.Add(new EmailRow { Email = new EmailAddressRecord("[a]") }); + var config = new QueryKitConfiguration(config => + { + config.Property(x => x.Email!).HasConversion(); + }); + + // Act + var result = rows.ApplyQueryKitFilter(input, config).ToList(); + + // Assert + result.Select(x => x.Email!.Value).Should().BeEquivalentTo("[a]"); + } + + [Fact] + public void list_item_that_does_not_convert_with_has_conversion_throws_parsing_exception() + { + // Arrange + var config = new QueryKitConfiguration(config => + { + config.Property(x => x.Id).HasConversion(); + }); + + // Act + var act = () => WrappedIdRows().ApplyQueryKitFilter("""Id ^^ ["2","abc"]""", config).ToList(); + + // Assert + var exception = act.Should().Throw().Which; + exception.Value.Should().Be("abc"); + exception.PropertyName.Should().Be("Id"); + exception.TargetType.Should().Be(typeof(WrappedId)); + } + private static List WrappedIdRows() => new() { new() { Id = new WrappedId(1), Name = "one" }, diff --git a/QueryKit/FilterParser.cs b/QueryKit/FilterParser.cs index 5f56321..503b2de 100644 --- a/QueryKit/FilterParser.cs +++ b/QueryKit/FilterParser.cs @@ -637,6 +637,15 @@ private static Expression CreateRightExprForProperty(Expression leftExpr, string var stringCtor = (underlyingType ?? leftExpr.Type).GetConstructor(new[] { typeof(string) }); if (stringCtor != null) { + if (op.TakesList() && IsListValue(right, rightIsQuotedLiteral, op)) + { + var constructedType = underlyingType ?? leftExpr.Type; + var items = SplitListItems(right).Select(item => Expression.Constant( + ConvertValue(item, constructedType, () => stringCtor.Invoke(BindingFlags.DoNotWrapExceptions, null, new object[] { item }, null)), + leftExpr.Type)); + return Expression.NewArrayInit(leftExpr.Type, items); + } + Expression constructed = Expression.New(stringCtor, FilterValue.Create(right, typeof(string))); return underlyingType == null ? constructed : Expression.Convert(constructed, leftExpr.Type); }