├── .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 |
--------------------------------------------------------------------------------