Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions QueryKit.IntegrationTests/Tests/DatabaseFilteringTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,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()
{
Expand Down
35 changes: 4 additions & 31 deletions QueryKit.UnitTests/FilterParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Recipe>(input);
act.Should().ThrowExactly<NullReferenceException>();
}

[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<Recipe>(input, config);
act.Should().ThrowExactly<NullReferenceException>();
}

[Fact]
public void member_after_a_child_collection_member_resolves_in_any_case()
{
var input = """Ingredients.Preparations.text == "sifted" """;
var act = () => FilterParser.ParseFilter<Recipe>(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<Recipe>(input);
act.Should().ThrowExactly<NullReferenceException>();
var filterExpression = FilterParser.ParseFilter<Recipe>(input);
filterExpression.ToDisplayString().Should()
.Be(""""x => x.Ingredients.Select(y => y.Name).Any(z => (z == "flour"))"""");
}

[Fact]
Expand Down
18 changes: 2 additions & 16 deletions QueryKit/PropertyResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>();
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)
{
Expand Down
Loading