@@ -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}
0 commit comments