Skip to content

Commit 0de191a

Browse files
committed
feat: custom operations
resolves #50
1 parent d501a55 commit 0de191a

File tree

5 files changed

+585
-0
lines changed

5 files changed

+585
-0
lines changed

QueryKit.IntegrationTests/Tests/DatabaseFilteringTests.cs

Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2428,4 +2428,342 @@ public async Task can_filter_with_nested_arithmetic_expressions()
24282428
people[0].Age.Should().Be(age);
24292429
people[0].Rating.Should().Be(rating);
24302430
}
2431+
2432+
[Fact]
2433+
public async Task can_filter_with_custom_operation_simple_calculation()
2434+
{
2435+
// Arrange
2436+
var testingServiceScope = new TestingServiceScope();
2437+
var faker = new Faker();
2438+
2439+
var age = 25;
2440+
var rating = 4;
2441+
var uniqueTitle = $"CustomOpTest{Guid.NewGuid()}";
2442+
2443+
var fakePerson = new FakeTestingPersonBuilder()
2444+
.WithAge(age)
2445+
.WithRating(rating)
2446+
.WithTitle(uniqueTitle)
2447+
.Build();
2448+
2449+
await testingServiceScope.InsertAsync(fakePerson);
2450+
2451+
var input = $"""ageTimeRating > 99 && Title == "{uniqueTitle}" """;
2452+
2453+
var config = new QueryKitConfiguration(config =>
2454+
{
2455+
config.CustomOperation<TestingPerson>((x, op, value) => (x.Age * x.Rating) > (decimal)value)
2456+
.HasQueryName("ageTimeRating");
2457+
});
2458+
2459+
// Act
2460+
var queryablePeople = testingServiceScope.DbContext().People;
2461+
var appliedQueryable = queryablePeople.ApplyQueryKitFilter(input, config);
2462+
var people = await appliedQueryable.ToListAsync();
2463+
2464+
// Assert - Age (25) * Rating (4) = 100 > 99 = true
2465+
people.Count.Should().Be(1);
2466+
people[0].Id.Should().Be(fakePerson.Id);
2467+
people[0].Age.Should().Be(age);
2468+
people[0].Rating.Should().Be(rating);
2469+
}
2470+
2471+
[Fact]
2472+
public async Task can_filter_with_custom_operation_different_operators()
2473+
{
2474+
// Arrange
2475+
var testingServiceScope = new TestingServiceScope();
2476+
var faker = new Faker();
2477+
2478+
var age = 30;
2479+
var rating = 5;
2480+
var uniqueTitle = $"CustomOpDiffOpsTest{Guid.NewGuid()}";
2481+
2482+
var fakePerson = new FakeTestingPersonBuilder()
2483+
.WithAge(age)
2484+
.WithRating(rating)
2485+
.WithTitle(uniqueTitle)
2486+
.Build();
2487+
2488+
await testingServiceScope.InsertAsync(fakePerson);
2489+
2490+
var input = $"""agePlusRating == 35 && Title == "{uniqueTitle}" """;
2491+
2492+
var config = new QueryKitConfiguration(config =>
2493+
{
2494+
config.CustomOperation<TestingPerson>((x, op, value) => (x.Age + x.Rating) == (decimal)value)
2495+
.HasQueryName("agePlusRating");
2496+
});
2497+
2498+
// Act
2499+
var queryablePeople = testingServiceScope.DbContext().People;
2500+
var appliedQueryable = queryablePeople.ApplyQueryKitFilter(input, config);
2501+
var people = await appliedQueryable.ToListAsync();
2502+
2503+
// Assert - Age (30) + Rating (5) = 35 == 35 = true
2504+
people.Count.Should().Be(1);
2505+
people[0].Id.Should().Be(fakePerson.Id);
2506+
people[0].Age.Should().Be(age);
2507+
people[0].Rating.Should().Be(rating);
2508+
}
2509+
2510+
[Fact]
2511+
public async Task can_filter_with_custom_operation_recipe_ingredient_quality()
2512+
{
2513+
// Arrange
2514+
var testingServiceScope = new TestingServiceScope();
2515+
var faker = new Faker();
2516+
2517+
var highQualityIngredient = new FakeIngredientBuilder()
2518+
.WithQualityLevel(8)
2519+
.Build();
2520+
var lowQualityIngredient = new FakeIngredientBuilder()
2521+
.WithQualityLevel(3)
2522+
.Build();
2523+
2524+
var recipe = new FakeRecipeBuilder().Build();
2525+
recipe.AddIngredient(highQualityIngredient);
2526+
recipe.AddIngredient(lowQualityIngredient);
2527+
2528+
await testingServiceScope.InsertAsync(recipe);
2529+
2530+
var input = $"""avgQuality > 5 && Title == "{recipe.Title}" """;
2531+
2532+
var config = new QueryKitConfiguration(config =>
2533+
{
2534+
config.CustomOperation<Recipe>((x, op, value) =>
2535+
x.Ingredients.Average(i => i.QualityLevel ?? 0) > (double)value)
2536+
.HasQueryName("avgQuality");
2537+
});
2538+
2539+
// Act
2540+
var queryableRecipes = testingServiceScope.DbContext().Recipes;
2541+
var appliedQueryable = queryableRecipes.ApplyQueryKitFilter(input, config);
2542+
var recipes = await appliedQueryable.ToListAsync();
2543+
2544+
// Assert - Average quality (8+3)/2 = 5.5 > 5 = true
2545+
recipes.Count.Should().Be(1);
2546+
recipes[0].Id.Should().Be(recipe.Id);
2547+
}
2548+
2549+
[Fact]
2550+
public async Task can_filter_with_custom_operation_complex_business_logic()
2551+
{
2552+
// Arrange
2553+
var testingServiceScope = new TestingServiceScope();
2554+
var faker = new Faker();
2555+
2556+
var age = 35;
2557+
var rating = 8;
2558+
var firstName = "VIP";
2559+
var uniqueTitle = $"ComplexBusinessTest{Guid.NewGuid()}";
2560+
2561+
var fakePerson = new FakeTestingPersonBuilder()
2562+
.WithAge(age)
2563+
.WithRating(rating)
2564+
.WithFirstName(firstName)
2565+
.WithTitle(uniqueTitle)
2566+
.Build();
2567+
2568+
await testingServiceScope.InsertAsync(fakePerson);
2569+
2570+
var input = $"""isVipCustomer == true && Title == "{uniqueTitle}" """;
2571+
2572+
var config = new QueryKitConfiguration(config =>
2573+
{
2574+
config.CustomOperation<TestingPerson>((x, op, value) =>
2575+
(bool)value ?
2576+
(x.Age > 30 && x.Rating > 7 && x.FirstName.Contains("VIP")) :
2577+
!(x.Age > 30 && x.Rating > 7 && x.FirstName.Contains("VIP")))
2578+
.HasQueryName("isVipCustomer");
2579+
});
2580+
2581+
// Act
2582+
var queryablePeople = testingServiceScope.DbContext().People;
2583+
var appliedQueryable = queryablePeople.ApplyQueryKitFilter(input, config);
2584+
var people = await appliedQueryable.ToListAsync();
2585+
2586+
// Assert - Age > 30 (35) AND Rating > 7 (8) AND FirstName contains "VIP" = true
2587+
people.Count.Should().Be(1);
2588+
people[0].Id.Should().Be(fakePerson.Id);
2589+
people[0].Age.Should().Be(age);
2590+
people[0].Rating.Should().Be(rating);
2591+
people[0].FirstName.Should().Be(firstName);
2592+
}
2593+
2594+
[Fact]
2595+
public async Task can_filter_with_custom_operation_combined_with_regular_filters()
2596+
{
2597+
// Arrange
2598+
var testingServiceScope = new TestingServiceScope();
2599+
var faker = new Faker();
2600+
2601+
var age = 40;
2602+
var rating = 6;
2603+
var lastName = "Smith";
2604+
var uniqueTitle = $"CombinedFiltersTest{Guid.NewGuid()}";
2605+
2606+
var fakePerson = new FakeTestingPersonBuilder()
2607+
.WithAge(age)
2608+
.WithRating(rating)
2609+
.WithLastName(lastName)
2610+
.WithTitle(uniqueTitle)
2611+
.Build();
2612+
2613+
await testingServiceScope.InsertAsync(fakePerson);
2614+
2615+
var input = $"""ageRatingProduct > 200 && LastName == "{lastName}" && Title == "{uniqueTitle}" """;
2616+
2617+
var config = new QueryKitConfiguration(config =>
2618+
{
2619+
config.CustomOperation<TestingPerson>((x, op, value) => (x.Age * x.Rating) > (decimal)value)
2620+
.HasQueryName("ageRatingProduct");
2621+
});
2622+
2623+
// Act
2624+
var queryablePeople = testingServiceScope.DbContext().People;
2625+
var appliedQueryable = queryablePeople.ApplyQueryKitFilter(input, config);
2626+
var people = await appliedQueryable.ToListAsync();
2627+
2628+
// Assert - Custom: Age (40) * Rating (6) = 240 > 200 AND Regular: LastName == "Smith"
2629+
people.Count.Should().Be(1);
2630+
people[0].Id.Should().Be(fakePerson.Id);
2631+
people[0].Age.Should().Be(age);
2632+
people[0].Rating.Should().Be(rating);
2633+
people[0].LastName.Should().Be(lastName);
2634+
}
2635+
2636+
[Fact]
2637+
public async Task can_filter_with_custom_operation_using_logical_operators()
2638+
{
2639+
// Arrange
2640+
var testingServiceScope = new TestingServiceScope();
2641+
var faker = new Faker();
2642+
2643+
var uniqueTitle1 = "LogicalTest1";
2644+
var uniqueTitle2 = "LogicalTest2";
2645+
2646+
var personOne = new FakeTestingPersonBuilder()
2647+
.WithAge(25)
2648+
.WithRating(8)
2649+
.WithTitle(uniqueTitle1)
2650+
.Build();
2651+
2652+
var personTwo = new FakeTestingPersonBuilder()
2653+
.WithAge(45)
2654+
.WithRating(3)
2655+
.WithTitle(uniqueTitle2)
2656+
.Build();
2657+
2658+
await testingServiceScope.InsertAsync(personOne, personTwo);
2659+
2660+
var input = $"""highScore == true && Title == "{uniqueTitle1}" """;
2661+
2662+
var config = new QueryKitConfiguration(config =>
2663+
{
2664+
config.CustomOperation<TestingPerson>((x, op, value) =>
2665+
(bool)value ? (x.Age * x.Rating) > 150 : (x.Age * x.Rating) <= 150)
2666+
.HasQueryName("highScore");
2667+
});
2668+
2669+
// Act
2670+
var queryablePeople = testingServiceScope.DbContext().People;
2671+
var appliedQueryable = queryablePeople.ApplyQueryKitFilter(input, config);
2672+
var people = await appliedQueryable.ToListAsync();
2673+
2674+
// Assert - Person1: 25*8=200>150 (highScore=true) AND Title matches = true
2675+
people.Count.Should().Be(1);
2676+
people.Should().Contain(p => p.Id == personOne.Id);
2677+
}
2678+
2679+
[Fact]
2680+
public async Task can_filter_with_custom_operation_date_handling()
2681+
{
2682+
// Arrange
2683+
var testingServiceScope = new TestingServiceScope();
2684+
var faker = new Faker();
2685+
2686+
var baseDate = new DateTime(2023, 6, 15, 0, 0, 0, DateTimeKind.Utc);
2687+
var recentDate = baseDate.AddDays(-5); // 5 days before base date
2688+
var oldDate = baseDate.AddDays(-15); // 15 days before base date
2689+
var cutoffDate = baseDate.AddDays(-10); // 10 days before base date
2690+
2691+
var recentPerson = new FakeTestingPersonBuilder()
2692+
.WithSpecificDateTime(recentDate)
2693+
.WithTitle("RecentUser")
2694+
.Build();
2695+
2696+
var oldPerson = new FakeTestingPersonBuilder()
2697+
.WithSpecificDateTime(oldDate)
2698+
.WithTitle("OldUser")
2699+
.Build();
2700+
2701+
await testingServiceScope.InsertAsync(recentPerson, oldPerson);
2702+
2703+
var input = $"""isRecentUser == true && Title == "RecentUser" """;
2704+
2705+
var config = new QueryKitConfiguration(config =>
2706+
{
2707+
config.CustomOperation<TestingPerson>((x, op, value) =>
2708+
(bool)value ?
2709+
x.SpecificDateTime > baseDate.AddDays(-10) :
2710+
x.SpecificDateTime <= baseDate.AddDays(-10))
2711+
.HasQueryName("isRecentUser");
2712+
});
2713+
2714+
// Act
2715+
var queryablePeople = testingServiceScope.DbContext().People;
2716+
var appliedQueryable = queryablePeople.ApplyQueryKitFilter(input, config);
2717+
var people = await appliedQueryable.ToListAsync();
2718+
2719+
// Assert - Only the recent user should match (SpecificDateTime > 10 days ago)
2720+
people.Count.Should().Be(1);
2721+
people.Should().Contain(p => p.Id == recentPerson.Id);
2722+
people.Should().NotContain(p => p.Id == oldPerson.Id);
2723+
people[0].SpecificDateTime.Should().BeAfter(cutoffDate);
2724+
}
2725+
2726+
[Fact]
2727+
public async Task can_filter_with_custom_operation_date_parameter()
2728+
{
2729+
// Arrange
2730+
var testingServiceScope = new TestingServiceScope();
2731+
var faker = new Faker();
2732+
2733+
var targetDate = new DateTime(2023, 6, 15, 0, 0, 0, DateTimeKind.Utc);
2734+
var beforeDate = targetDate.AddDays(-1);
2735+
var afterDate = targetDate.AddDays(1);
2736+
2737+
var beforePerson = new FakeTestingPersonBuilder()
2738+
.WithSpecificDateTime(beforeDate)
2739+
.WithTitle("BeforeUser")
2740+
.Build();
2741+
2742+
var afterPerson = new FakeTestingPersonBuilder()
2743+
.WithSpecificDateTime(afterDate)
2744+
.WithTitle("AfterUser")
2745+
.Build();
2746+
2747+
await testingServiceScope.InsertAsync(beforePerson, afterPerson);
2748+
2749+
var input = """isAfterDate == "2023-06-15T00:00:00Z" """;
2750+
2751+
var config = new QueryKitConfiguration(config =>
2752+
{
2753+
config.CustomOperation<TestingPerson>((x, op, value) =>
2754+
x.SpecificDateTime > (DateTime)value)
2755+
.HasQueryName("isAfterDate");
2756+
});
2757+
2758+
// Act
2759+
var queryablePeople = testingServiceScope.DbContext().People;
2760+
var appliedQueryable = queryablePeople.ApplyQueryKitFilter(input, config);
2761+
var people = await appliedQueryable.ToListAsync();
2762+
2763+
// Assert - Only the person after the target date should match
2764+
people.Count.Should().Be(1);
2765+
people.Should().Contain(p => p.Id == afterPerson.Id);
2766+
people.Should().NotContain(p => p.Id == beforePerson.Id);
2767+
people[0].SpecificDateTime.Should().BeAfter(targetDate);
2768+
}
24312769
}

QueryKit/Configuration/QueryKitSettings.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,9 @@ public QueryKitPropertyMapping<TModel> DerivedProperty<TModel>(Expression<Func<T
4545
{
4646
return PropertyMappings.DerivedProperty(propertySelector);
4747
}
48+
49+
public QueryKitCustomOperationMapping<TModel> CustomOperation<TModel>(Expression<Func<TModel, ComparisonOperator, object, bool>> operationExpression)
50+
{
51+
return PropertyMappings.CustomOperation(operationExpression);
52+
}
4853
}

0 commit comments

Comments
 (0)