diff --git a/QueryKit.IntegrationTests/Tests/DatabaseFilteringTests.cs b/QueryKit.IntegrationTests/Tests/DatabaseFilteringTests.cs index a41b3da..4524c0b 100644 --- a/QueryKit.IntegrationTests/Tests/DatabaseFilteringTests.cs +++ b/QueryKit.IntegrationTests/Tests/DatabaseFilteringTests.cs @@ -255,6 +255,37 @@ public async Task can_filter_by_string_for_collection() recipes[0].Id.Should().Be(fakeRecipeOne.Id); } + [Fact] + public async Task can_filter_by_child_collection_member_in_another_case() + { + // Arrange + var testingServiceScope = new TestingServiceScope(); + var faker = new Faker(); + var fakeIngredientOne = new FakeIngredientBuilder() + .WithName(faker.Lorem.Sentence()) + .Build(); + var fakeRecipeOne = new FakeRecipeBuilder().Build(); + fakeRecipeOne.AddIngredient(fakeIngredientOne); + + var fakeIngredientTwo = new FakeIngredientBuilder() + .WithName(faker.Lorem.Sentence()) + .Build(); + var fakeRecipeTwo = new FakeRecipeBuilder().Build(); + fakeRecipeTwo.AddIngredient(fakeIngredientTwo); + await testingServiceScope.InsertAsync(fakeRecipeOne, fakeRecipeTwo); + + var input = $"""ingredients.name == "{fakeIngredientOne.Name}" """; + + // Act + var queryableRecipes = testingServiceScope.DbContext().Recipes; + var appliedQueryable = queryableRecipes.ApplyQueryKitFilter(input); + var recipes = await appliedQueryable.ToListAsync(); + + // Assert + recipes.Count.Should().Be(1); + recipes[0].Id.Should().Be(fakeRecipeOne.Id); + } + [Fact] public async Task can_filter_by_numeric_string_for_collection() { diff --git a/QueryKit.UnitTests/FilterParserTests.cs b/QueryKit.UnitTests/FilterParserTests.cs index 3121e05..fc9823f 100644 --- a/QueryKit.UnitTests/FilterParserTests.cs +++ b/QueryKit.UnitTests/FilterParserTests.cs @@ -556,39 +556,12 @@ public void simple_child_collection_for_string_equal() } [Fact] - public void child_collection_member_in_another_case_throws() + public void child_collection_member_resolves_in_any_case() { var input = """ingredients.name == "flour" """; - var act = () => FilterParser.ParseFilter(input); - act.Should().ThrowExactly(); - } - - [Fact] - public void unknown_child_collection_member_throws_when_unknown_properties_are_allowed() - { - var input = """Ingredients.Nope == "flour" """; - var config = new QueryKitConfiguration(config => - { - config.AllowUnknownProperties = true; - }); - var act = () => FilterParser.ParseFilter(input, config); - act.Should().ThrowExactly(); - } - - [Fact] - public void member_after_a_child_collection_member_resolves_in_any_case() - { - var input = """Ingredients.Preparations.text == "sifted" """; - var act = () => FilterParser.ParseFilter(input); - act.Should().NotThrow(); - } - - [Fact] - public void nested_child_collection_member_in_another_case_throws() - { - var input = """Ingredients.preparations.Text == "sifted" """; - var act = () => FilterParser.ParseFilter(input); - act.Should().ThrowExactly(); + var filterExpression = FilterParser.ParseFilter(input); + filterExpression.ToDisplayString().Should() + .Be(""""x => x.Ingredients.Select(y => y.Name).Any(z => (z == "flour"))""""); } [Fact] diff --git a/QueryKit/PropertyResolver.cs b/QueryKit/PropertyResolver.cs index 4645ef5..e2ebde0 100644 --- a/QueryKit/PropertyResolver.cs +++ b/QueryKit/PropertyResolver.cs @@ -78,37 +78,23 @@ internal static PropertyReference Resolve(Type rootType, string reference, IQuer // Matches each segment to a member, ignoring case, in the order of Expression.PropertyOrField like v1.14.2: // a public property, a public field, a non-public property, and then a non-public field. An indexer does not match. - // A segment after a collection resolves on the element type. - // After a collection, only public properties match: the first segment in the exact case, a later segment in any case. - // A segment after a collection that does not match throws NullReferenceException. + // A segment after a collection resolves on the element type, with the same rules. private static string? ResolveMemberPath(Type rootType, string path, out string? unknownSegment) { var memberNames = new List(); var currentType = rootType; - var afterCollection = false; foreach (var segment in path.Split('.')) { - var firstAfterCollection = !afterCollection && memberNames.Count > 0 && IsCollection(currentType); while (IsCollection(currentType)) { currentType = currentType.GetGenericArguments()[0]; } - MemberInfo? member; - if (firstAfterCollection || afterCollection) - { - member = (firstAfterCollection ? currentType.GetProperty(segment) : currentType.GetProperty(segment, PublicMemberFlags)) - ?? throw new NullReferenceException(); - afterCollection = true; - } - else - { - member = (MemberInfo?)currentType.GetProperty(segment, PublicMemberFlags) + var member = (MemberInfo?)currentType.GetProperty(segment, PublicMemberFlags) ?? (MemberInfo?)currentType.GetField(segment, PublicMemberFlags) ?? (MemberInfo?)currentType.GetProperty(segment, NonPublicMemberFlags) ?? currentType.GetField(segment, NonPublicMemberFlags); - } if (member == null || member is PropertyInfo indexer && indexer.GetIndexParameters().Length > 0) {