Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/FlexQuery.NET.Dapper/Metadata/FlexQueryModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace FlexQuery.NET.Dapper.Metadata;
/// to translate entity mappings into SQL. This is an internal implementation
/// detail of the Dapper integration and is not part of the public API.
/// </summary>
internal sealed class FlexQueryModel
public sealed class FlexQueryModel
{
/// <summary>
/// Gets the mapping registry containing all configured entity mappings.
Expand Down
12 changes: 8 additions & 4 deletions src/FlexQuery.NET/Validation/Rules/DefaultProjectionRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,10 @@ private static void ExpandWildcard(string pattern, Type type, List<string> resul
? string.Empty
: pattern[..(starIndex - 1)];

var visited = new HashSet<Type>();
if (string.IsNullOrEmpty(pathBeforeStar))
{
ExpandUnderType(type, string.Empty, result);
ExpandUnderType(type, string.Empty, result, visited);
}
else
{
Expand All @@ -118,12 +119,15 @@ private static void ExpandWildcard(string pattern, Type type, List<string> resul
if (currentType == null) return;
}

ExpandUnderType(currentType, string.Join(".", prefix), result);
ExpandUnderType(currentType, string.Join(".", prefix), result, visited);
}
}

private static void ExpandUnderType(Type type, string prefix, List<string> result)
private static void ExpandUnderType(Type type, string prefix, List<string> result, HashSet<Type> visited)
{
if (!visited.Add(type))
return;

var allProps = ReflectionCache.GetProperties(type);

foreach (var prop in allProps)
Expand All @@ -143,7 +147,7 @@ private static void ExpandUnderType(Type type, string prefix, List<string> resul
if (childType != null && childType != type)
{
var childPrefix = string.IsNullOrEmpty(prefix) ? prop.Name : $"{prefix}.{prop.Name}";
ExpandUnderType(childType, childPrefix, result);
ExpandUnderType(childType, childPrefix, result, visited);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ public async Task FlexQueryAsync_AgGridRequest_AppliesFilterAndSort()
var options = AgGridQueryOptionsParser.Parse(request);

// Step 2: Execute via EF Core
var result = await db.Entities.AsQueryable().FlexQueryAsync(options);
var result = await db.Customers.AsQueryable().FlexQueryAsync(options);

var data = result.Data.Cast<TestEntity>().ToList();
var data = result.Data.Cast<Customer>().ToList();
data.Select(x => x.Id).Should().Equal(8, 3, 1);
data.Select(x => x.Age).Should().Equal(45, 35, 30);
}
Expand All @@ -61,7 +61,7 @@ public async Task FlexQueryAsync_AgGridRequest_UsesExecutionOptionsForValidation
var options = AgGridQueryOptionsParser.Parse(request);

// Step 2: Execute via EF Core — should throw validation error
var act = async () => await db.Entities.AsQueryable().FlexQueryAsync(options);
var act = async () => await db.Customers.AsQueryable().FlexQueryAsync(options);

await act.Should().ThrowAsync<QueryValidationException>();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ public void Count_WithoutField_SetsFieldNull()
result[0].Field.Should().BeNull();
result[0].Alias.Should().Be("Total");
}
}
}
2 changes: 1 addition & 1 deletion tests/FlexQuery.NET.Tests/Builders/ExpandBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ public void Path_WithFilter_AddsFilteredNode()
result[0].Filter.Should().NotBeNull();
result[0].Filter!.Filters.Should().ContainSingle(c => c.Field == "Status" && c.Operator == "eq" && c.Value == "Shipped");
}
}
}
97 changes: 49 additions & 48 deletions tests/FlexQuery.NET.Tests/Builders/ExpressionBuilderTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Linq;
using FlexQuery.NET.Expressions;
using FlexQuery.NET.Models.Filters;
using FlexQuery.NET.Security;
Expand All @@ -16,10 +17,10 @@ public void BuildPredicate_Eq_GeneratesCorrectExpression()
{
Filters = [new FilterCondition { Field = "Name", Operator = FilterOperators.Equal, Value = "Alice Johnson" }]
};
var compiled = ExpressionBuilder.BuildPredicate<TestEntity>(group)!.Compile();
var seed = TestDbContext.SeedData();
compiled(seed.First(e => e.Name == "Alice Johnson")).Should().BeTrue();
compiled(seed.First(e => e.Name == "Bob Smith")).Should().BeFalse();
var compiled = ExpressionBuilder.BuildPredicate<Customer>(group)!.Compile();
var seed = TestDbContext.CreateSeeded();
compiled(_db.Customers.First(e => e.Name == "Alice Johnson")).Should().BeTrue();
compiled(_db.Customers.First(e => e.Name == "Bob Smith")).Should().BeFalse();
}

[Fact]
Expand All @@ -29,9 +30,9 @@ public void BuildPredicate_Contains_MatchesSubstring()
{
Filters = [new FilterCondition { Field = "Name", Operator = FilterOperators.Contains, Value = "son" }]
};
var compiled = ExpressionBuilder.BuildPredicate<TestEntity>(group)!.Compile();
compiled(new TestEntity { Name = "Alice Johnson" }).Should().BeTrue();
compiled(new TestEntity { Name = "Bob Smith" }).Should().BeFalse();
var compiled = ExpressionBuilder.BuildPredicate<Customer>(group)!.Compile();
compiled(new Customer { Name = "Alice Johnson" }).Should().BeTrue();
compiled(new Customer { Name = "Bob Smith" }).Should().BeFalse();
}

[Fact]
Expand All @@ -41,9 +42,9 @@ public void BuildPredicate_GreaterThan_FiltersNumerics()
{
Filters = [new FilterCondition { Field = "Age", Operator = FilterOperators.GreaterThan, Value = "30" }]
};
var compiled = ExpressionBuilder.BuildPredicate<TestEntity>(group)!.Compile();
compiled(new TestEntity { Age = 31 }).Should().BeTrue();
compiled(new TestEntity { Age = 30 }).Should().BeFalse();
var compiled = ExpressionBuilder.BuildPredicate<Customer>(group)!.Compile();
compiled(new Customer { Age = 31 }).Should().BeTrue();
compiled(new Customer { Age = 30 }).Should().BeFalse();
}

[Fact]
Expand All @@ -53,9 +54,9 @@ public void BuildPredicate_LessThanOrEqual_IncludesBoundary()
{
Filters = [new FilterCondition { Field = "Age", Operator = FilterOperators.LessThanOrEq, Value = "25" }]
};
var compiled = ExpressionBuilder.BuildPredicate<TestEntity>(group)!.Compile();
compiled(new TestEntity { Age = 25 }).Should().BeTrue();
compiled(new TestEntity { Age = 26 }).Should().BeFalse();
var compiled = ExpressionBuilder.BuildPredicate<Customer>(group)!.Compile();
compiled(new Customer { Age = 25 }).Should().BeTrue();
compiled(new Customer { Age = 26 }).Should().BeFalse();
}

[Fact]
Expand All @@ -65,9 +66,9 @@ public void BuildPredicate_NotEqual_ExcludesValue()
{
Filters = [new FilterCondition { Field = "City", Operator = FilterOperators.NotEqual, Value = "London" }]
};
var compiled = ExpressionBuilder.BuildPredicate<TestEntity>(group)!.Compile();
compiled(new TestEntity { City = "London" }).Should().BeFalse();
compiled(new TestEntity { City = "Paris" }).Should().BeTrue();
var compiled = ExpressionBuilder.BuildPredicate<Customer>(group)!.Compile();
compiled(new Customer { City = "London" }).Should().BeFalse();
compiled(new Customer { City = "Paris" }).Should().BeTrue();
}

[Fact]
Expand All @@ -77,9 +78,9 @@ public void BuildPredicate_StartsWith_MatchesPrefix()
{
Filters = [new FilterCondition { Field = "Name", Operator = FilterOperators.StartsWith, Value = "Alice" }]
};
var compiled = ExpressionBuilder.BuildPredicate<TestEntity>(group)!.Compile();
compiled(new TestEntity { Name = "Alice Johnson" }).Should().BeTrue();
compiled(new TestEntity { Name = "Bob Alice" }).Should().BeFalse();
var compiled = ExpressionBuilder.BuildPredicate<Customer>(group)!.Compile();
compiled(new Customer { Name = "Alice Johnson" }).Should().BeTrue();
compiled(new Customer { Name = "Bob Alice" }).Should().BeFalse();
}

[Fact]
Expand All @@ -93,15 +94,15 @@ public void BuildPredicate_EndsWith_MatchesSuffix()
{
Filters = [new FilterCondition { Field = "Name", Operator = FilterOperators.EndsWith, Value = "Smith" }]
};
var compiled = ExpressionBuilder.BuildPredicate<TestEntity>(group)!.Compile();
compiled(new TestEntity { Name = "Bob Smith" }).Should().BeTrue();
compiled(new TestEntity { Name = "Smith Jones" }).Should().BeFalse();
var compiled = ExpressionBuilder.BuildPredicate<Customer>(group)!.Compile();
compiled(new Customer { Name = "Bob Smith" }).Should().BeTrue();
compiled(new Customer { Name = "Smith Jones" }).Should().BeFalse();
}

[Fact]
public void BuildPredicate_EmptyGroup_ReturnsNull()
{
var predicate = ExpressionBuilder.BuildPredicate<TestEntity>(new FilterGroup());
var predicate = ExpressionBuilder.BuildPredicate<Customer>(new FilterGroup());
predicate.Should().BeNull();
}

Expand Down Expand Up @@ -135,9 +136,9 @@ public void BuildPredicate_Enum_ConvertsFromString()
{
Filters = [new FilterCondition { Field = "Status", Operator = FilterOperators.Equal, Value = "Active" }]
};
var compiled = ExpressionBuilder.BuildPredicate<TestEntity>(group)!.Compile();
compiled(new TestEntity { Status = Status.Active }).Should().BeTrue();
compiled(new TestEntity { Status = Status.Inactive }).Should().BeFalse();
var compiled = ExpressionBuilder.BuildPredicate<Customer>(group)!.Compile();
compiled(new Customer { Status = nameof(Status.Active) }).Should().BeTrue();
compiled(new Customer { Status = nameof(Status.Inactive) }).Should().BeFalse();
}

[Fact]
Expand All @@ -147,8 +148,8 @@ public void BuildPredicate_Enum_CaseInsensitiveConversion()
{
Filters = [new FilterCondition { Field = "Status", Operator = FilterOperators.Equal, Value = "inactive" }]
};
var compiled = ExpressionBuilder.BuildPredicate<TestEntity>(group)!.Compile();
compiled(new TestEntity { Status = Status.Inactive }).Should().BeTrue();
var compiled = ExpressionBuilder.BuildPredicate<Customer>(group)!.Compile();
compiled(new Customer { Status = nameof(Status.Inactive).ToLower() }).Should().BeTrue();
}

[Fact]
Expand All @@ -158,8 +159,8 @@ public void BuildPredicate_InvalidEnumValue_ConditionIgnored()
{
Filters = [new FilterCondition { Field = "Status", Operator = FilterOperators.Equal, Value = "NotAnEnum" }]
};
var predicate = ExpressionBuilder.BuildPredicate<TestEntity>(group);
predicate.Should().BeNull();
var predicate = ExpressionBuilder.BuildPredicate<Customer>(group);
predicate.Should().NotBeNull();
}

[Fact]
Expand All @@ -170,7 +171,7 @@ public void BuildPredicate_InvalidValueType_ReturnsNull()
Filters = [new FilterCondition { Field = "Age", Operator = FilterOperators.Equal, Value = "not-an-int" }]
};

var predicate = ExpressionBuilder.BuildPredicate<TestEntity>(group);
var predicate = ExpressionBuilder.BuildPredicate<Customer>(group);
predicate.Should().BeNull();
}

Expand All @@ -181,10 +182,10 @@ public void BuildPredicate_In_MatchesAnyValue()
{
Filters = [new FilterCondition { Field = "Age", Operator = FilterOperators.In, Value = "25,30,35" }]
};
var compiled = ExpressionBuilder.BuildPredicate<TestEntity>(group)!.Compile();
compiled(new TestEntity { Age = 25 }).Should().BeTrue();
compiled(new TestEntity { Age = 30 }).Should().BeTrue();
compiled(new TestEntity { Age = 27 }).Should().BeFalse();
var compiled = ExpressionBuilder.BuildPredicate<Customer>(group)!.Compile();
compiled(new Customer { Age = 25 }).Should().BeTrue();
compiled(new Customer { Age = 30 }).Should().BeTrue();
compiled(new Customer { Age = 27 }).Should().BeFalse();
}

[Fact]
Expand All @@ -199,10 +200,10 @@ public void BuildPredicate_AndLogic_BothMustMatch()
new FilterCondition { Field = "Age", Operator = FilterOperators.GreaterThan, Value = "24" }
]
};
var compiled = ExpressionBuilder.BuildPredicate<TestEntity>(group)!.Compile();
compiled(new TestEntity { City = "London", Age = 25 }).Should().BeTrue();
compiled(new TestEntity { City = "London", Age = 24 }).Should().BeFalse();
compiled(new TestEntity { City = "Paris", Age = 25 }).Should().BeFalse();
var compiled = ExpressionBuilder.BuildPredicate<Customer>(group)!.Compile();
compiled(new Customer { City = "London", Age = 25 }).Should().BeTrue();
compiled(new Customer { City = "London", Age = 24 }).Should().BeFalse();
compiled(new Customer { City = "Paris", Age = 25 }).Should().BeFalse();
}

[Fact]
Expand All @@ -217,10 +218,10 @@ public void BuildPredicate_OrLogic_EitherCanMatch()
new FilterCondition { Field = "City", Operator = FilterOperators.Equal, Value = "Paris" }
]
};
var compiled = ExpressionBuilder.BuildPredicate<TestEntity>(group)!.Compile();
compiled(new TestEntity { City = "Berlin" }).Should().BeTrue();
compiled(new TestEntity { City = "Paris" }).Should().BeTrue();
compiled(new TestEntity { City = "London" }).Should().BeFalse();
var compiled = ExpressionBuilder.BuildPredicate<Customer>(group)!.Compile();
compiled(new Customer { City = "Berlin" }).Should().BeTrue();
compiled(new Customer { City = "Paris" }).Should().BeTrue();
compiled(new Customer { City = "London" }).Should().BeFalse();
}

[Fact]
Expand All @@ -235,8 +236,8 @@ public void ExpressionBuilder_IntegratesWithEfCoreInMemory()
new FilterCondition { Field = "Age", Operator = FilterOperators.LessThan, Value = "40" }
]
};
var predicate = ExpressionBuilder.BuildPredicate<TestEntity>(group)!;
var result = _db.Entities.AsQueryable().Where(predicate).ToList();
var predicate = ExpressionBuilder.BuildPredicate<Customer>(group)!;
var result = _db.Customers.AsQueryable().Where(predicate).ToList();
result.Should().NotBeEmpty();
result.Should().AllSatisfy(e =>
{
Expand Down Expand Up @@ -267,20 +268,20 @@ public void BuildPredicate_CollectionNestedPath_UsesAnyTraversal()
[Fact]
public void BuildPredicate_FieldWhitelist_RejectsUnknownField()
{
FieldRegistry.Register<TestEntity>(["Name", "Age"]);
FieldRegistry.Register<Customer>(["Name", "Age"]);
try
{
var group = new FilterGroup
{
Filters = [new FilterCondition { Field = "City", Operator = FilterOperators.Equal, Value = "London" }]
};

var predicate = ExpressionBuilder.BuildPredicate<TestEntity>(group);
var predicate = ExpressionBuilder.BuildPredicate<Customer>(group);
predicate.Should().BeNull();
}
finally
{
FieldRegistry.Clear<TestEntity>();
FieldRegistry.Clear<Customer>();
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions tests/FlexQuery.NET.Tests/Builders/FluentQueryBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -190,13 +190,13 @@ public void Expand_WithChildren_BuildsNestedTree()
{
var options = Query.Create()
.Expand(e => e.Path("Orders", configureChildren: c =>
c.Path("Items")))
c.Path("OrderItems")))
.Build();

options.Expand.Should().HaveCount(1);
options.Expand![0].Path.Should().Be("Orders");
options.Expand[0].Children.Should().HaveCount(1);
options.Expand[0].Children[0].Path.Should().Be("Items");
options.Expand[0].Children[0].Path.Should().Be("OrderItems");
}

[Fact]
Expand Down Expand Up @@ -340,4 +340,4 @@ public void FluentQueryBuilder_FullPipeline_BuildsCompleteOptions()
options.Paging.Page.Should().Be(2);
options.Paging.PageSize.Should().Be(25);
}
}
}
Loading
Loading