├── .editorconfig ├── .gitignore ├── LICENSE ├── QueryEvaluationInterceptor.sln ├── QueryEvaluationInterceptor ├── .editorconfig ├── BinaryInterceptorVisitor.cs ├── CustomQueryProvider.cs ├── ExpressionTransformer.cs ├── GuardRailsExpressionVisitor.cs ├── ICustomQueryProvider.cs ├── IQueryHost.cs ├── IQueryInterceptingProvider.cs ├── IQueryInterceptor.cs ├── Program.cs ├── QueryEvaluationInterceptor.csproj ├── QueryHost.cs ├── QueryInterceptingProvider.cs ├── Thing.cs └── stylecop.json └── README.md /.editorconfig: -------------------------------------------------------------------------------- 1 | # To learn more about .editorconfig see https://aka.ms/editorconfigdocs 2 | ############################### 3 | # Core EditorConfig Options # 4 | ############################### 5 | root = true 6 | # All files 7 | [*] 8 | indent_style = space 9 | # Code files 10 | [*.{cs,csx,vb,vbx}] 11 | indent_size = 4 12 | trim_trailing_whitespace = true; 13 | insert_final_newline = true 14 | charset = utf-8-bom 15 | ############################### 16 | # .NET Coding Conventions # 17 | ############################### 18 | [*.{cs,vb}] 19 | # Organize usings 20 | dotnet_sort_system_directives_first = true 21 | 22 | # this. preferences 23 | dotnet_style_qualification_for_field = false:silent 24 | dotnet_style_qualification_for_property = false:silent 25 | dotnet_style_qualification_for_method = false:silent 26 | dotnet_style_qualification_for_event = false:silent 27 | 28 | # Language keywords vs BCL types preferences 29 | dotnet_style_predefined_type_for_locals_parameters_members = true:silent 30 | dotnet_style_predefined_type_for_member_access = true:silent 31 | 32 | # Parentheses preferences 33 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent 34 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent 35 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent 36 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent 37 | 38 | # Modifier preferences 39 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent 40 | dotnet_style_readonly_field = true:suggestion 41 | 42 | # Expression-level preferences 43 | dotnet_style_object_initializer = true:suggestion 44 | dotnet_style_collection_initializer = true:suggestion 45 | dotnet_style_explicit_tuple_names = true:suggestion 46 | dotnet_style_null_propagation = true:suggestion 47 | dotnet_style_coalesce_expression = true:suggestion 48 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent 49 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 50 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 51 | dotnet_style_prefer_auto_properties = true:silent 52 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 53 | dotnet_style_prefer_conditional_expression_over_return = true:silent 54 | 55 | ############################### 56 | # Naming Conventions # 57 | ############################### 58 | 59 | # Style Definitions 60 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 61 | 62 | # Use PascalCase for constant fields 63 | dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion 64 | dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields 65 | dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style 66 | dotnet_naming_symbols.constant_fields.applicable_kinds = field 67 | dotnet_naming_symbols.constant_fields.applicable_accessibilities = * 68 | dotnet_naming_symbols.constant_fields.required_modifiers = const 69 | 70 | ############################### 71 | # C# Coding Conventions # 72 | ############################### 73 | [*.cs] 74 | 75 | # var preferences 76 | csharp_style_var_for_built_in_types = true:silent 77 | csharp_style_var_when_type_is_apparent = true:silent 78 | csharp_style_var_elsewhere = true:silent 79 | 80 | # Expression-bodied members 81 | csharp_style_expression_bodied_methods = false:silent 82 | csharp_style_expression_bodied_constructors = false:silent 83 | csharp_style_expression_bodied_operators = false:silent 84 | csharp_style_expression_bodied_properties = true:silent 85 | csharp_style_expression_bodied_indexers = true:silent 86 | csharp_style_expression_bodied_accessors = true:silent 87 | 88 | # Pattern matching preferences 89 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 90 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 91 | 92 | # Null-checking preferences 93 | csharp_style_throw_expression = true:suggestion 94 | csharp_style_conditional_delegate_call = true:suggestion 95 | 96 | # Modifier preferences 97 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion 98 | 99 | # Expression-level preferences 100 | csharp_prefer_braces = true:silent 101 | csharp_style_deconstructed_variable_declaration = true:suggestion 102 | csharp_prefer_simple_default_expression = true:suggestion 103 | csharp_style_pattern_local_over_anonymous_function = true:suggestion 104 | csharp_style_inlined_variable_declaration = true:suggestion 105 | 106 | ############################### 107 | # C# Formatting Rules # 108 | ############################### 109 | 110 | # New line preferences 111 | csharp_new_line_before_open_brace = all 112 | csharp_new_line_before_else = true 113 | csharp_new_line_before_catch = true 114 | csharp_new_line_before_finally = true 115 | csharp_new_line_before_members_in_object_initializers = true 116 | csharp_new_line_before_members_in_anonymous_types = true 117 | csharp_new_line_between_query_expression_clauses = true 118 | 119 | # Indentation preferences 120 | csharp_indent_case_contents = true 121 | csharp_indent_switch_labels = true 122 | csharp_indent_labels = flush_left 123 | 124 | # Space preferences 125 | csharp_space_after_cast = false 126 | csharp_space_after_keywords_in_control_flow_statements = true 127 | csharp_space_between_method_call_parameter_list_parentheses = false 128 | csharp_space_between_method_declaration_parameter_list_parentheses = false 129 | csharp_space_between_parentheses = false 130 | csharp_space_before_colon_in_inheritance_clause = true 131 | csharp_space_after_colon_in_inheritance_clause = true 132 | csharp_space_around_binary_operators = before_and_after 133 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 134 | csharp_space_between_method_call_name_and_opening_parenthesis = false 135 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 136 | 137 | # Wrapping preferences 138 | csharp_preserve_single_line_statements = true 139 | csharp_preserve_single_line_blocks = true 140 | 141 | ############################### 142 | # VB Coding Conventions # 143 | ############################### 144 | 145 | # SA1200: Using directives should be placed correctly 146 | dotnet_diagnostic.SA1200.severity = none 147 | 148 | # SA1101: Prefix local calls with this 149 | dotnet_diagnostic.SA1101.severity = silent 150 | 151 | [*.vb] 152 | # Modifier preferences 153 | visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion 154 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore these files 2 | 3 | # Visual Studio options folder 4 | .vs/ 5 | 6 | # Build objects 7 | obj/ 8 | 9 | # Packages 10 | packages/ 11 | 12 | # Build assets 13 | bin/ 14 | 15 | # Test results 16 | TestResults/ 17 | 18 | # Docs log 19 | log.txt -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jeremy Likness 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /QueryEvaluationInterceptor.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30406.217 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QueryEvaluationInterceptor", "QueryEvaluationInterceptor\QueryEvaluationInterceptor.csproj", "{22FA7EFD-1757-490C-A0D0-252D874A64AC}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{583CD691-CA20-4EE8-844E-C0CA94AD033B}" 9 | ProjectSection(SolutionItems) = preProject 10 | .gitignore = .gitignore 11 | LICENSE = LICENSE 12 | EndProjectSection 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {22FA7EFD-1757-490C-A0D0-252D874A64AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {22FA7EFD-1757-490C-A0D0-252D874A64AC}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {22FA7EFD-1757-490C-A0D0-252D874A64AC}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {22FA7EFD-1757-490C-A0D0-252D874A64AC}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {8437D831-9558-4CFA-B940-DA632A9A3D67} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /QueryEvaluationInterceptor/.editorconfig: -------------------------------------------------------------------------------- 1 | # To learn more about .editorconfig see https://aka.ms/editorconfigdocs 2 | ############################### 3 | # Core EditorConfig Options # 4 | ############################### 5 | root = true 6 | # All files 7 | [*] 8 | indent_style = space 9 | # Code files 10 | [*.{cs,csx,vb,vbx}] 11 | indent_size = 4 12 | trim_trailing_whitespace = true; 13 | insert_final_newline = true 14 | charset = utf-8-bom 15 | ############################### 16 | # .NET Coding Conventions # 17 | ############################### 18 | [*.{cs,vb}] 19 | # Organize usings 20 | dotnet_sort_system_directives_first = true 21 | 22 | # this. preferences 23 | dotnet_style_qualification_for_field = false:silent 24 | dotnet_style_qualification_for_property = false:silent 25 | dotnet_style_qualification_for_method = false:silent 26 | dotnet_style_qualification_for_event = false:silent 27 | 28 | # Language keywords vs BCL types preferences 29 | dotnet_style_predefined_type_for_locals_parameters_members = true:silent 30 | dotnet_style_predefined_type_for_member_access = true:silent 31 | 32 | # Parentheses preferences 33 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent 34 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent 35 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent 36 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent 37 | 38 | # Modifier preferences 39 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent 40 | dotnet_style_readonly_field = true:suggestion 41 | 42 | # Expression-level preferences 43 | dotnet_style_object_initializer = true:suggestion 44 | dotnet_style_collection_initializer = true:suggestion 45 | dotnet_style_explicit_tuple_names = true:suggestion 46 | dotnet_style_null_propagation = true:suggestion 47 | dotnet_style_coalesce_expression = true:suggestion 48 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent 49 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 50 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 51 | dotnet_style_prefer_auto_properties = true:silent 52 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 53 | dotnet_style_prefer_conditional_expression_over_return = true:silent 54 | 55 | ############################### 56 | # Naming Conventions # 57 | ############################### 58 | 59 | # Style Definitions 60 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 61 | 62 | # Use PascalCase for constant fields 63 | dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion 64 | dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields 65 | dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style 66 | dotnet_naming_symbols.constant_fields.applicable_kinds = field 67 | dotnet_naming_symbols.constant_fields.applicable_accessibilities = * 68 | dotnet_naming_symbols.constant_fields.required_modifiers = const 69 | 70 | ############################### 71 | # C# Coding Conventions # 72 | ############################### 73 | [*.cs] 74 | 75 | # var preferences 76 | csharp_style_var_for_built_in_types = true:silent 77 | csharp_style_var_when_type_is_apparent = true:silent 78 | csharp_style_var_elsewhere = true:silent 79 | 80 | # Expression-bodied members 81 | csharp_style_expression_bodied_methods = false:silent 82 | csharp_style_expression_bodied_constructors = false:silent 83 | csharp_style_expression_bodied_operators = false:silent 84 | csharp_style_expression_bodied_properties = true:silent 85 | csharp_style_expression_bodied_indexers = true:silent 86 | csharp_style_expression_bodied_accessors = true:silent 87 | 88 | # Pattern matching preferences 89 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 90 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 91 | 92 | # Null-checking preferences 93 | csharp_style_throw_expression = true:suggestion 94 | csharp_style_conditional_delegate_call = true:suggestion 95 | 96 | # Modifier preferences 97 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion 98 | 99 | # Expression-level preferences 100 | csharp_prefer_braces = true:silent 101 | csharp_style_deconstructed_variable_declaration = true:suggestion 102 | csharp_prefer_simple_default_expression = true:suggestion 103 | csharp_style_pattern_local_over_anonymous_function = true:suggestion 104 | csharp_style_inlined_variable_declaration = true:suggestion 105 | 106 | ############################### 107 | # C# Formatting Rules # 108 | ############################### 109 | 110 | # New line preferences 111 | csharp_new_line_before_open_brace = all 112 | csharp_new_line_before_else = true 113 | csharp_new_line_before_catch = true 114 | csharp_new_line_before_finally = true 115 | csharp_new_line_before_members_in_object_initializers = true 116 | csharp_new_line_before_members_in_anonymous_types = true 117 | csharp_new_line_between_query_expression_clauses = true 118 | 119 | # Indentation preferences 120 | csharp_indent_case_contents = true 121 | csharp_indent_switch_labels = true 122 | csharp_indent_labels = flush_left 123 | 124 | # Space preferences 125 | csharp_space_after_cast = false 126 | csharp_space_after_keywords_in_control_flow_statements = true 127 | csharp_space_between_method_call_parameter_list_parentheses = false 128 | csharp_space_between_method_declaration_parameter_list_parentheses = false 129 | csharp_space_between_parentheses = false 130 | csharp_space_before_colon_in_inheritance_clause = true 131 | csharp_space_after_colon_in_inheritance_clause = true 132 | csharp_space_around_binary_operators = before_and_after 133 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 134 | csharp_space_between_method_call_name_and_opening_parenthesis = false 135 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 136 | 137 | # Wrapping preferences 138 | csharp_preserve_single_line_statements = true 139 | csharp_preserve_single_line_blocks = true 140 | 141 | ############################### 142 | # VB Coding Conventions # 143 | ############################### 144 | 145 | # SA1200: Using directives should be placed correctly 146 | dotnet_diagnostic.SA1200.severity = none 147 | 148 | # SA1101: Prefix local calls with this 149 | dotnet_diagnostic.SA1101.severity = silent 150 | 151 | [*.vb] 152 | # Modifier preferences 153 | visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion 154 | -------------------------------------------------------------------------------- /QueryEvaluationInterceptor/BinaryInterceptorVisitor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System; 5 | using System.Linq; 6 | using System.Linq.Expressions; 7 | using System.Reflection; 8 | 9 | namespace QueryEvaluationInterceptor 10 | { 11 | /// 12 | /// Interceptor that prints the evaluation of binary expressions. 13 | /// 14 | /// The type of target to intercept. 15 | public class BinaryInterceptorVisitor : ExpressionVisitor 16 | { 17 | /// 18 | /// The to get a static method. 19 | /// 20 | private static readonly BindingFlags GetStatic = 21 | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static; 22 | 23 | /// 24 | /// The for the pre-evaluation method. 25 | /// 26 | private static readonly MethodInfo BeforeEvalMethod = GetMethod(nameof(BeforeEval)); 27 | 28 | /// 29 | /// The for the post-evaluation method. 30 | /// 31 | private static readonly MethodInfo AfterEvalMethod = GetMethod(nameof(AfterEval)); 32 | 33 | /// 34 | /// The for the method to capture the instsance. 35 | /// 36 | private static readonly MethodInfo SetInstanceMethod = GetMethod(nameof(SetInstance)); 37 | 38 | /// 39 | /// The last instance to be evaluated. 40 | /// 41 | private static object instance; 42 | 43 | /// 44 | /// Nested level during evaluation (execution). 45 | /// 46 | private static int evalLevel = 0; 47 | 48 | /// 49 | /// The nested level of binary expression during parsing. 50 | /// 51 | private int binaryLevel = 0; 52 | 53 | /// 54 | /// Gets the indent for console log. 55 | /// 56 | private static string Indent => new string('\t', evalLevel); 57 | 58 | /// 59 | /// Method to run before an expression is evaluated. 60 | /// 61 | /// The level of evaluation. 62 | /// The text of the evaluated node. 63 | public static void BeforeEval(int binaryLevel, string node) 64 | { 65 | if (binaryLevel == 1) 66 | { 67 | Console.WriteLine($"with {instance} => {{"); 68 | evalLevel = 0; 69 | } 70 | else 71 | { 72 | evalLevel++; 73 | } 74 | 75 | Console.WriteLine($"{Indent}[Eval {node}: "); 76 | } 77 | 78 | /// 79 | /// Method to run after evaluation. 80 | /// 81 | /// The nested level of expression. 82 | /// A value that indicates whether the evaluation was successful. 83 | public static void AfterEval(int binaryLevel, bool success) 84 | { 85 | var result = success ? "SUCCESS" : "FAILED"; 86 | 87 | Console.WriteLine($"{Indent}{result}]"); 88 | 89 | evalLevel--; 90 | 91 | if (binaryLevel == 1) 92 | { 93 | Console.WriteLine("}"); 94 | } 95 | } 96 | 97 | /// 98 | /// Visit and transform a . 99 | /// 100 | /// The to processs. 101 | /// The transformed . 102 | protected override Expression VisitBinary(BinaryExpression node) 103 | { 104 | binaryLevel++; 105 | 106 | // call the pre-evaluation method. 107 | var before = Expression.Call( 108 | BeforeEvalMethod, 109 | Expression.Constant(binaryLevel), 110 | Expression.Constant($"{node}")); 111 | 112 | // call post-evaluation with success. 113 | var afterSuccess = Expression.Call( 114 | AfterEvalMethod, 115 | Expression.Constant(binaryLevel), 116 | Expression.Constant(true)); 117 | 118 | // call post-evaluation with failure. 119 | var afterFailure = Expression.Call( 120 | AfterEvalMethod, 121 | Expression.Constant(binaryLevel), 122 | Expression.Constant(false)); 123 | 124 | // call pre-evaluation then return false to force the right-evaluation. 125 | var orLeft = Expression.Block( 126 | before, 127 | Expression.Constant(false)); 128 | 129 | // call post-evaluation and return true to preserve result. 130 | var andRight = Expression.Block( 131 | afterSuccess, 132 | Expression.Constant(true)); 133 | 134 | // call post-evaluation and return false to preserve result. 135 | var orRight = Expression.Block( 136 | afterFailure, 137 | Expression.Constant(false)); 138 | 139 | // get a parsed version of the expression. 140 | var binary = node.Update( 141 | Visit(node.Left), 142 | node.Conversion, 143 | Visit(node.Right)); 144 | 145 | binaryLevel--; 146 | 147 | // return PRE-EVAL=FALSE OR ((CONDITION AND POST-EVAL=SUCCESS) OR POST-EVAL=FAILURE) 148 | return Expression.OrElse( 149 | orLeft, 150 | Expression.OrElse( 151 | Expression.AndAlso(binary, andRight), 152 | orRight)); 153 | } 154 | 155 | /// 156 | /// Visit and transform a lambda expression. 157 | /// 158 | /// The type of the lambda. 159 | /// The original . 160 | /// The transformed . 161 | protected override Expression VisitLambda(Expression node) 162 | { 163 | // should be of type Func and match the type we're after 164 | if (node.Parameters.Count == 1 && 165 | node.Parameters[0].Type == typeof(T) && 166 | node.ReturnType == typeof(bool)) 167 | { 168 | // a place to put the result of the original 169 | var returnTarget = Expression.Label(typeof(bool)); 170 | 171 | // a copy of the lambda that's been recurisvely transformed 172 | var lambda = node.Update( 173 | Visit(node.Body), 174 | node.Parameters.Select(p => Visit(p)).Cast()); 175 | 176 | // call the original and capture the result 177 | var innerInvoke = Expression.Return( 178 | returnTarget, Expression.Invoke(lambda, lambda.Parameters)); 179 | 180 | // intercept the type, save it for reference, then call 181 | // the original lambda. The "false" is a default value that 182 | // is always overridden. 183 | var expr = Expression.Block( 184 | Expression.Call(SetInstanceMethod, node.Parameters), 185 | innerInvoke, 186 | Expression.Label(returnTarget, Expression.Constant(false))); 187 | 188 | // make it all into a lambda 189 | return Expression.Lambda>( 190 | expr, 191 | node.Parameters); 192 | } 193 | 194 | return base.VisitLambda(node); 195 | } 196 | 197 | /// 198 | /// Method to retrieve for static methods. 199 | /// 200 | /// The name of the method. 201 | /// The method's . 202 | private static MethodInfo GetMethod(string methodName) => 203 | typeof(BinaryInterceptorVisitor).GetMethod(methodName, GetStatic); 204 | 205 | /// 206 | /// Set the instance value. 207 | /// 208 | /// The value passed to the binary expressions. 209 | private static void SetInstance(object instance) 210 | { 211 | BinaryInterceptorVisitor.instance = instance; 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /QueryEvaluationInterceptor/CustomQueryProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Linq.Expressions; 7 | 8 | namespace QueryEvaluationInterceptor 9 | { 10 | /// 11 | /// Base query provider class. 12 | /// 13 | /// The entity type. 14 | public abstract class CustomQueryProvider : ICustomQueryProvider 15 | { 16 | /// 17 | /// Initializes a new instance of the class. 18 | /// 19 | /// The query to snapshot. 20 | public CustomQueryProvider(IQueryable sourceQuery) 21 | { 22 | Source = sourceQuery; 23 | } 24 | 25 | /// 26 | /// Gets the source . 27 | /// 28 | protected IQueryable Source { get; } 29 | 30 | /// 31 | /// Creates the query. 32 | /// 33 | /// The query . 34 | /// The query. 35 | public abstract IQueryable CreateQuery(Expression expression); 36 | 37 | /// 38 | /// Creates the query. 39 | /// 40 | /// The entity type. 41 | /// The query . 42 | /// The query. 43 | public abstract IQueryable CreateQuery(Expression expression); 44 | 45 | /// 46 | /// Runs the query and returns the result. 47 | /// 48 | /// The to use. 49 | /// The query result. 50 | public virtual object Execute(Expression expression) 51 | { 52 | return Source.Provider.Execute(expression); 53 | } 54 | 55 | /// 56 | /// Runs the query and returns the typed result. 57 | /// 58 | /// The type of the result. 59 | /// The query . 60 | /// The query result. 61 | public virtual TResult Execute(Expression expression) 62 | { 63 | object result = (this as IQueryProvider).Execute(expression); 64 | return (TResult)result; 65 | } 66 | 67 | /// 68 | /// Return the enumerable result. 69 | /// 70 | /// The to parse. 71 | /// The . 72 | /// Throw when expression is null. 73 | public virtual IEnumerable ExecuteEnumerable(Expression expression) 74 | { 75 | return Source.Provider.CreateQuery(expression); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /QueryEvaluationInterceptor/ExpressionTransformer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System.Linq.Expressions; 5 | 6 | namespace QueryEvaluationInterceptor 7 | { 8 | /// 9 | /// Transform one expression to another. 10 | /// 11 | /// The source . 12 | /// The transformed expression. 13 | public delegate Expression ExpressionTransformer(Expression source); 14 | } 15 | -------------------------------------------------------------------------------- /QueryEvaluationInterceptor/GuardRailsExpressionVisitor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System.Linq; 5 | using System.Linq.Expressions; 6 | 7 | namespace QueryEvaluationInterceptor 8 | { 9 | /// 10 | /// Transformation that ensures a Take(10) is applied. 11 | /// 12 | public class GuardRailsExpressionVisitor : ExpressionVisitor 13 | { 14 | /// 15 | /// Flag to track the first visit. 16 | /// 17 | private bool first = true; 18 | 19 | /// 20 | /// Gets a value indicating whether a Take expression was found. 21 | /// 22 | public bool TakeFound { get; private set; } 23 | 24 | /// 25 | /// Entry-level visitor. 26 | /// 27 | /// The to visit and transform. 28 | /// The transformed . 29 | public override Expression Visit(Expression node) 30 | { 31 | if (first) 32 | { 33 | first = false; 34 | var expr = base.Visit(node); 35 | 36 | // ensured existing take set to limit 37 | if (TakeFound) 38 | { 39 | return expr; 40 | } 41 | 42 | // no take, we'll need to add one 43 | var existing = expr as MethodCallExpression; 44 | 45 | // capture call to existing, then pass into "take(10)" 46 | var newExpression = Expression.Call( 47 | typeof(Queryable), 48 | nameof(Queryable.Take), 49 | existing.Method.ReturnType.GetGenericArguments(), 50 | existing, 51 | Expression.Constant(10)); 52 | return newExpression; 53 | } 54 | 55 | return base.Visit(node); 56 | } 57 | 58 | /// 59 | /// Inspects the to see if it 60 | /// is a "take". 61 | /// 62 | /// The to inspect. 63 | /// The transformed . 64 | protected override Expression VisitMethodCall(MethodCallExpression node) 65 | { 66 | if (node.Method.Name == nameof(Queryable.Take)) 67 | { 68 | TakeFound = true; 69 | 70 | // this will only work if a constant is passed. 71 | // to capture all scenarios would require recurisve parsing of the argument. 72 | if (node.Arguments[1] is ConstantExpression constant) 73 | { 74 | // make sure it's an integer 75 | if (constant.Value is int valueInt) 76 | { 77 | // only need to change it if it's too high 78 | // borrow the original first argument 79 | if (valueInt > 10) 80 | { 81 | var expression = node.Update( 82 | node.Object, 83 | new[] { node.Arguments[0] } 84 | .Append(Expression.Constant(10))); 85 | return expression; 86 | } 87 | } 88 | } 89 | } 90 | 91 | return base.VisitMethodCall(node); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /QueryEvaluationInterceptor/ICustomQueryProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Linq.Expressions; 7 | 8 | namespace QueryEvaluationInterceptor 9 | { 10 | /// 11 | /// Interface for a custom implementation of . 12 | /// 13 | /// The entity type. 14 | public interface ICustomQueryProvider : IQueryProvider 15 | { 16 | /// 17 | /// Execute enumeration from the . 18 | /// 19 | /// The to enumerate. 20 | /// An instance of . 21 | IEnumerable ExecuteEnumerable(Expression expression); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /QueryEvaluationInterceptor/IQueryHost.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System.Linq; 5 | 6 | namespace QueryEvaluationInterceptor 7 | { 8 | /// 9 | /// Interface for custom query host. 10 | /// 11 | /// The type of entity. 12 | /// The to handle logic. 13 | public interface IQueryHost : IOrderedQueryable, IOrderedQueryable 14 | where TProvider : ICustomQueryProvider 15 | { 16 | /// 17 | /// Gets the that handles the custom logic. 18 | /// 19 | TProvider CustomProvider { get; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /QueryEvaluationInterceptor/IQueryInterceptingProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System.Linq.Expressions; 5 | 6 | namespace QueryEvaluationInterceptor 7 | { 8 | /// 9 | /// Interface for provider that intercepts the when run. 10 | /// 11 | /// The type. 12 | public interface IQueryInterceptingProvider : IQueryInterceptor, ICustomQueryProvider 13 | { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /QueryEvaluationInterceptor/IQueryInterceptor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | namespace QueryEvaluationInterceptor 5 | { 6 | /// 7 | /// Exposes a method to register a transformation. 8 | /// 9 | public interface IQueryInterceptor 10 | { 11 | /// 12 | /// Register the transformation to intercept. 13 | /// 14 | /// The method to inspect and/or transform. 15 | void RegisterInterceptor(ExpressionTransformer transformation); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /QueryEvaluationInterceptor/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Data; 7 | using System.Linq; 8 | using System.Linq.Expressions; 9 | 10 | namespace QueryEvaluationInterceptor 11 | { 12 | /// 13 | /// Demo program. 14 | /// 15 | internal class Program 16 | { 17 | /// 18 | /// A "database" of 10,000 things. 19 | /// 20 | private static readonly List ThingDb = Thing.GetThings(10000); 21 | 22 | /// 23 | /// Gets a divider to write. 24 | /// 25 | private static readonly string Divider = new string('-', 80); 26 | 27 | /// 28 | /// Gets queryable things. 29 | /// 30 | private static IQueryable ThingDbQuery => ThingDb.AsQueryable(); 31 | 32 | /// 33 | /// Main method. 34 | /// 35 | private static void Main() 36 | { 37 | Console.WriteLine("Query interception examples."); 38 | 39 | Expression parser = () => RunParser(); 40 | Expression guardRails = () => RunGuardRails(); 41 | Expression interceptor = () => RunInterceptor(); 42 | 43 | foreach (var expr in new[] { parser, guardRails, interceptor }) 44 | { 45 | RunMethod(expr); 46 | } 47 | 48 | Console.WriteLine("Done."); 49 | } 50 | 51 | /// 52 | /// Runs a demo method. 53 | /// 54 | /// An expression that points to the method. 55 | private static void RunMethod(Expression method) 56 | { 57 | var methodToCall = (method.Body as MethodCallExpression) 58 | .Method.Name; 59 | Console.WriteLine(Divider); 60 | Console.WriteLine($"Running method: {methodToCall}()"); 61 | var action = method.Compile(); 62 | action(); 63 | Console.WriteLine(Divider); 64 | Console.WriteLine("ENTER to continue."); 65 | Console.ReadLine(); 66 | } 67 | 68 | /// 69 | /// Run the interceptor that evaluates binary expressions. 70 | /// 71 | private static void RunInterceptor() 72 | { 73 | Console.WriteLine("Intercepts calls to binary expressions."); 74 | 75 | static Expression ExpressionTransformer(Expression e) 76 | { 77 | Console.WriteLine(e); 78 | var newExpression = new BinaryInterceptorVisitor().Visit(e); 79 | return newExpression; 80 | } 81 | 82 | // trim the list 83 | var smallSample = ThingDbQuery.Take(15).ToList(); 84 | 85 | // wrap and intercept 86 | var query = new QueryHost(smallSample.AsQueryable()); 87 | query.CustomProvider.RegisterInterceptor(ExpressionTransformer); 88 | 89 | Console.WriteLine("About to run the query..."); 90 | 91 | var list = query.Where(t => t.IsTrue && 92 | t.Expires < DateTime.Now.AddDays(500)) 93 | .OrderBy(t => t.Id).ToList(); 94 | 95 | Console.WriteLine($"Retrieved {list.Count()} items."); 96 | } 97 | 98 | /// 99 | /// Run the example that limits returned items. 100 | /// 101 | private static void RunGuardRails() 102 | { 103 | Console.WriteLine("Forces the query to 10 items."); 104 | 105 | static Expression ExpressionTransformer(Expression e) 106 | { 107 | Console.WriteLine($"Before: {e}"); 108 | 109 | var newExpression = new GuardRailsExpressionVisitor().Visit(e); 110 | 111 | Console.WriteLine($"After: {newExpression}"); 112 | return newExpression; 113 | } 114 | 115 | // wrap and intercept 116 | var query = new QueryHost(ThingDbQuery); 117 | query.CustomProvider.RegisterInterceptor(ExpressionTransformer); 118 | 119 | RunMethod( 120 | query, 121 | q => q.Where(t => t.IsTrue && 122 | t.Id.Contains("aa") && 123 | t.Expires < DateTime.Now.AddDays(100)) 124 | .OrderBy(t => t.Id), 125 | "no take"); 126 | 127 | RunMethod( 128 | query, 129 | q => q.Where(t => t.IsTrue && 130 | t.Id.Contains("aa") && 131 | t.Expires < DateTime.Now.AddDays(100)) 132 | .OrderBy(t => t.Id).Take(50), 133 | "take 50"); 134 | 135 | RunMethod( 136 | query, 137 | q => q.Where(t => t.IsTrue && 138 | t.Id.Contains("aa") && 139 | t.Expires < DateTime.Now.AddDays(100)) 140 | .OrderBy(t => t.Id).Take(5), 141 | "take 5"); 142 | } 143 | 144 | /// 145 | /// Runs a query method. 146 | /// 147 | /// The . 148 | /// The filters to apply. 149 | /// The message to show. 150 | private static void RunMethod( 151 | QueryHost query, 152 | Func, IQueryable> filters, 153 | string message) 154 | { 155 | Console.WriteLine("---"); 156 | Console.WriteLine($"About to run the query ({message})..."); 157 | 158 | var list = filters(query).ToList(); 159 | 160 | Console.WriteLine($"Retrieved {list.Count()} items."); 161 | } 162 | 163 | /// 164 | /// Runs the simple parser demo. 165 | /// 166 | private static void RunParser() 167 | { 168 | Console.WriteLine("Shows the final query."); 169 | 170 | static Expression ExpressionTransformer(Expression e) 171 | { 172 | Console.WriteLine(e); 173 | return e; 174 | } 175 | 176 | // wrap and intercept 177 | var query = new QueryHost(ThingDbQuery); 178 | query.CustomProvider.RegisterInterceptor(ExpressionTransformer); 179 | 180 | Console.WriteLine("About to run the query..."); 181 | 182 | var list = query.Where(t => t.IsTrue && 183 | t.Id.Contains("aa") && 184 | t.Expires < DateTime.Now.AddDays(100)) 185 | .OrderBy(t => t.Id).ToList(); 186 | 187 | Console.WriteLine($"Retrieved {list.Count()} items."); 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /QueryEvaluationInterceptor/QueryEvaluationInterceptor.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | true 7 | 8 | 9 | 10 | 11 | all 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | 14 | 15 | 16 | 17 | 18 | Never 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /QueryEvaluationInterceptor/QueryHost.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Linq.Expressions; 9 | 10 | namespace QueryEvaluationInterceptor 11 | { 12 | /// 13 | /// Base class for custom query host. 14 | /// 15 | /// The entity type. 16 | public class QueryHost : IQueryHost> 17 | { 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | /// The original query. 22 | public QueryHost( 23 | IQueryable source) 24 | { 25 | Expression = source.Expression; 26 | CustomProvider = new QueryInterceptingProvider(source); 27 | } 28 | 29 | /// 30 | /// Initializes a new instance of the class. 31 | /// 32 | /// The . 33 | /// The . 34 | public QueryHost( 35 | Expression expression, 36 | QueryInterceptingProvider provider) 37 | { 38 | Expression = expression; 39 | CustomProvider = provider; 40 | } 41 | 42 | /// 43 | /// Gets the type of element. 44 | /// 45 | public virtual Type ElementType => typeof(T); 46 | 47 | /// 48 | /// Gets the for the query. 49 | /// 50 | public virtual Expression Expression { get; } 51 | 52 | /// 53 | /// Gets the instance of the . 54 | /// 55 | public IQueryProvider Provider => CustomProvider; 56 | 57 | /// 58 | /// Gets or sets the instance of the . 59 | /// 60 | public IQueryInterceptingProvider CustomProvider { get; protected set; } 61 | 62 | /// 63 | /// Gets an for the query results. 64 | /// 65 | /// The . 66 | public virtual IEnumerator GetEnumerator() => 67 | CustomProvider.ExecuteEnumerable(Expression).GetEnumerator(); 68 | 69 | /// 70 | /// Ignoring the explicit implementation. 71 | /// 72 | /// The . 73 | /// Thrown every call. 74 | IEnumerator IEnumerable.GetEnumerator() 75 | { 76 | throw new NotImplementedException(); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /QueryEvaluationInterceptor/QueryInterceptingProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Linq.Expressions; 8 | 9 | namespace QueryEvaluationInterceptor 10 | { 11 | /// 12 | /// Provider that intercepts the when run. 13 | /// 14 | /// The entity type. 15 | public class QueryInterceptingProvider : 16 | CustomQueryProvider, IQueryInterceptingProvider 17 | { 18 | /// 19 | /// The transformation to apply. 20 | /// 21 | private ExpressionTransformer transformation = null; 22 | 23 | /// 24 | /// Initializes a new instance of the class. 25 | /// 26 | /// The query to snapshot. 27 | public QueryInterceptingProvider(IQueryable sourceQuery) 28 | : base(sourceQuery) 29 | { 30 | } 31 | 32 | /// 33 | /// Creates a query host with this provider. 34 | /// 35 | /// The to use. 36 | /// The . 37 | public override IQueryable CreateQuery(Expression expression) 38 | { 39 | return new QueryHost(expression, this); 40 | } 41 | 42 | /// 43 | /// Creates a query host with a different type. 44 | /// 45 | /// The entity type. 46 | /// The to use. 47 | /// The . 48 | public override IQueryable CreateQuery(Expression expression) 49 | { 50 | if (typeof(TElement) == typeof(T)) 51 | { 52 | return CreateQuery(expression) as IQueryable; 53 | } 54 | 55 | var childProvider = new QueryInterceptingProvider(Source); 56 | 57 | return new QueryHost( 58 | expression, childProvider); 59 | } 60 | 61 | /// 62 | /// Registers the transformation to apply. 63 | /// 64 | /// A method that transforms an . 65 | public void RegisterInterceptor(ExpressionTransformer transformation) 66 | { 67 | if (this.transformation != null) 68 | { 69 | throw new InvalidOperationException(); 70 | } 71 | 72 | this.transformation = transformation; 73 | } 74 | 75 | /// 76 | /// Execute with transformation. 77 | /// 78 | /// The base . 79 | /// Result of executing the transformed expression. 80 | public override object Execute(Expression expression) 81 | { 82 | return Source.Provider.Execute(TransformExpression(expression)); 83 | } 84 | 85 | /// 86 | /// Execute the enumerable. 87 | /// 88 | /// The to execute. 89 | /// The result of the transformed expression. 90 | public override IEnumerable ExecuteEnumerable(Expression expression) 91 | { 92 | return base.ExecuteEnumerable(TransformExpression(expression)); 93 | } 94 | 95 | /// 96 | /// Perform the transformation. 97 | /// 98 | /// The original . 99 | /// The transformed . 100 | private Expression TransformExpression(Expression source) => 101 | transformation == null ? source : 102 | transformation(source); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /QueryEvaluationInterceptor/Thing.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jeremy Likness. All rights reserved. 2 | // Licensed under the MIT License. See LICENSE in the repository root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | 7 | namespace QueryEvaluationInterceptor 8 | { 9 | /// 10 | /// An example . 11 | /// 12 | public class Thing 13 | { 14 | /// 15 | /// Uncertainty. 16 | /// 17 | private static readonly Random Random = new Random(); 18 | 19 | /// 20 | /// Gets the unique id. 21 | /// 22 | /// 23 | /// Generated from a . 24 | /// 25 | public string Id { get; private set; } = Guid.NewGuid().ToString(); 26 | 27 | /// 28 | /// Gets an integer value. 29 | /// 30 | public int Value { get; private set; } = Random.Next(int.MinValue, int.MaxValue); 31 | 32 | /// 33 | /// Gets the time it was created. 34 | /// 35 | public DateTime Created { get; private set; } = DateTime.Now; 36 | 37 | /// 38 | /// Gets the time it expires. 39 | /// 40 | public DateTime Expires { get; private set; } = DateTime.Now.AddDays(Random.Next(1, 999)); 41 | 42 | /// 43 | /// Gets a value indicating whether true is truly true. 44 | /// 45 | public bool IsTrue { get; private set; } = Random.NextDouble() < 0.5; 46 | 47 | /// 48 | /// Generate a bunch of instances. 49 | /// 50 | /// The number of things. 51 | /// The of . 52 | public static List GetThings(int count) 53 | { 54 | var result = new List(); 55 | while (count-- > 0) 56 | { 57 | result.Add(new Thing()); 58 | } 59 | 60 | return result; 61 | } 62 | 63 | /// 64 | /// Print details about the . 65 | /// 66 | /// The values. 67 | public override string ToString() => 68 | $"Thing: Id={Id}, Value={Value}, Created={Created}, Expires={Expires}, IsTrue={IsTrue}"; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /QueryEvaluationInterceptor/stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 3 | "settings": { 4 | "documentationRules": { 5 | "companyName": "Jeremy Likness", 6 | "copyrightText": "Copyright (c) {companyName}. All rights reserved.\nLicensed under the MIT License. See LICENSE in the repository root for license information.", 7 | "xmlHeader": false, 8 | "documentInterfaces": true, 9 | "documentInternalElements": false 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QueryEvaluationInterceptor 2 | 3 | An example of intercepting IQueryable executions to parse and transform the query. 4 | 5 | 👉 Read the related blog post: [Inspect and Mutate IQueryable Expression Trees](https://blog.jeremylikness.com/blog/inspect-and-mutate-iqueryable-expression-trees/) 6 | --------------------------------------------------------------------------------