| | | 1 | | using System.Linq.Expressions; |
| | | 2 | | |
| | | 3 | | namespace NGql.Core.Builders; |
| | | 4 | | |
| | | 5 | | /// <summary> |
| | | 6 | | /// Extracts field paths from C# expression trees. |
| | | 7 | | /// Supports both compile-time lambda expressions and runtime-parsed expressions. |
| | | 8 | | /// </summary> |
| | | 9 | | public static class ExpressionFieldExtractor |
| | | 10 | | { |
| | | 11 | | /// <summary> |
| | | 12 | | /// Extracts field paths from a typed predicate expression. |
| | | 13 | | /// </summary> |
| | | 14 | | /// <typeparam name="T">The type being queried</typeparam> |
| | | 15 | | /// <param name="predicate">The predicate expression (e.g., x => x.user.profile.age > 10)</param> |
| | | 16 | | /// <returns>Set of field paths referenced in the expression</returns> |
| | | 17 | | /// <example> |
| | | 18 | | /// <code> |
| | | 19 | | /// var paths = ExpressionFieldExtractor.ExtractFieldPaths<MyModel>(x => x.user.profile.age > 10); |
| | | 20 | | /// // Returns: ["user.profile.age"] |
| | | 21 | | /// </code> |
| | | 22 | | /// </example> |
| | | 23 | | public static HashSet<string> ExtractFieldPaths<T>(Expression<Func<T, bool>> predicate) |
| | 201 | 24 | | => ExtractFieldPaths((Expression)predicate); |
| | | 25 | | |
| | | 26 | | /// <summary> |
| | | 27 | | /// Extracts field paths from a typed selector expression. |
| | | 28 | | /// Useful for selecting specific fields without a predicate. |
| | | 29 | | /// </summary> |
| | | 30 | | /// <typeparam name="T">The type being queried</typeparam> |
| | | 31 | | /// <typeparam name="TResult">The result type (can be object for anonymous types)</typeparam> |
| | | 32 | | /// <param name="selector">The selector expression (e.g., x => x.user.profile)</param> |
| | | 33 | | /// <returns>Set of field paths referenced in the expression</returns> |
| | | 34 | | /// <example> |
| | | 35 | | /// <code> |
| | | 36 | | /// var paths = ExpressionFieldExtractor.ExtractFieldPaths<MyModel, object>(x => new { x.user.name, x.user.ema |
| | | 37 | | /// // Returns: ["user.name", "user.email"] |
| | | 38 | | /// </code> |
| | | 39 | | /// </example> |
| | | 40 | | public static HashSet<string> ExtractFieldPaths<T, TResult>(Expression<Func<T, TResult>> selector) |
| | 42 | 41 | | => ExtractFieldPaths((Expression)selector); |
| | | 42 | | |
| | | 43 | | /// <summary> |
| | | 44 | | /// Extracts field paths from any expression. |
| | | 45 | | /// Works with runtime-parsed expressions (e.g., from DynamicExpresso). |
| | | 46 | | /// </summary> |
| | | 47 | | /// <param name="expression">The expression to analyze</param> |
| | | 48 | | /// <returns>Set of field paths referenced in the expression</returns> |
| | | 49 | | /// <example> |
| | | 50 | | /// <code> |
| | | 51 | | /// // With DynamicExpresso (in tests or application code): |
| | | 52 | | /// var interpreter = new Interpreter(); |
| | | 53 | | /// var expr = interpreter.Parse("user.profile.email != null"); |
| | | 54 | | /// var paths = ExpressionFieldExtractor.ExtractFieldPaths(expr); |
| | | 55 | | /// // Returns: ["user.profile.email"] |
| | | 56 | | /// </code> |
| | | 57 | | /// </example> |
| | | 58 | | public static HashSet<string> ExtractFieldPaths(Expression expression) |
| | | 59 | | { |
| | 429 | 60 | | var visitor = new FieldPathVisitor(); |
| | 429 | 61 | | visitor.Visit(expression); |
| | 429 | 62 | | return visitor.FieldPaths; |
| | | 63 | | } |
| | | 64 | | |
| | | 65 | | /// <summary> |
| | | 66 | | /// Internal visitor that walks the expression tree and collects field paths. |
| | | 67 | | /// </summary> |
| | | 68 | | private sealed class FieldPathVisitor : ExpressionVisitor |
| | | 69 | | { |
| | 1518 | 70 | | public HashSet<string> FieldPaths { get; } = new(StringComparer.OrdinalIgnoreCase); |
| | 429 | 71 | | private readonly Stack<string> _lambdaContextPaths = new(); |
| | | 72 | | private ParameterExpression? _rootParameter; |
| | 429 | 73 | | private readonly HashSet<Type> _rootParameterTypes = new(); |
| | | 74 | | |
| | | 75 | | /// <summary> |
| | | 76 | | /// Visits member access expressions (e.g., user.profile.age). |
| | | 77 | | /// Only collects complete leaf paths to avoid collecting intermediate segments. |
| | | 78 | | /// </summary> |
| | | 79 | | protected override Expression VisitMember(MemberExpression node) |
| | | 80 | | { |
| | | 81 | | // Skip properties that should be excluded (like string.Length) |
| | 675 | 82 | | if (ShouldExcludeProperty(node)) |
| | | 83 | | { |
| | | 84 | | // Still visit the expression to extract paths from null coalescing chains |
| | 42 | 85 | | VisitNullCoalescingChain(node.Expression); |
| | 42 | 86 | | return node; |
| | | 87 | | } |
| | | 88 | | |
| | 633 | 89 | | var path = BuildMemberPath(node); |
| | | 90 | | |
| | 633 | 91 | | if (!string.IsNullOrEmpty(path)) |
| | | 92 | | { |
| | | 93 | | // If we're inside a lambda context (e.g., LINQ predicate), prepend the base path |
| | 627 | 94 | | if (_lambdaContextPaths.Count > 0) |
| | | 95 | | { |
| | 63 | 96 | | var basePath = _lambdaContextPaths.Peek(); |
| | 63 | 97 | | path = $"{basePath}.{path}"; |
| | | 98 | | } |
| | | 99 | | |
| | 627 | 100 | | FieldPaths.Add(path); |
| | | 101 | | } |
| | | 102 | | |
| | | 103 | | // Visit any method calls in the chain (e.g., First(), Where()) |
| | | 104 | | // This ensures lambda contexts are established properly |
| | 633 | 105 | | VisitMethodCallsInChain(node); |
| | | 106 | | |
| | 633 | 107 | | return node; |
| | | 108 | | } |
| | | 109 | | |
| | | 110 | | /// <summary> |
| | | 111 | | /// Visits null coalescing chains to extract all field paths. |
| | | 112 | | /// This handles cases like (x.user.profile.name ?? x.user.email ?? "default").Length |
| | | 113 | | /// where we want to extract both user.profile.name and user.email. |
| | | 114 | | /// </summary> |
| | | 115 | | private void VisitNullCoalescingChain(Expression? expression) |
| | | 116 | | { |
| | 42 | 117 | | if (expression is BinaryExpression binaryExpr && binaryExpr.NodeType == ExpressionType.Coalesce) |
| | | 118 | | { |
| | | 119 | | // Visit both sides of the null coalescing operator |
| | 9 | 120 | | Visit(binaryExpr.Left); |
| | 9 | 121 | | Visit(binaryExpr.Right); |
| | | 122 | | } |
| | 33 | 123 | | else if (expression != null) |
| | | 124 | | { |
| | | 125 | | // Visit the expression normally |
| | 33 | 126 | | Visit(expression); |
| | | 127 | | } |
| | 33 | 128 | | } |
| | | 129 | | |
| | | 130 | | /// <summary> |
| | | 131 | | /// Visits any method call expressions in a member expression chain. |
| | | 132 | | /// This is necessary to establish lambda contexts for LINQ methods. |
| | | 133 | | /// </summary> |
| | | 134 | | private void VisitMethodCallsInChain(MemberExpression node) |
| | | 135 | | { |
| | 633 | 136 | | Expression? current = node.Expression; |
| | | 137 | | |
| | 1410 | 138 | | while (current != null) |
| | | 139 | | { |
| | 1410 | 140 | | if (current is MethodCallExpression methodCall) |
| | | 141 | | { |
| | | 142 | | // Visit the method call to process its lambda arguments |
| | 33 | 143 | | Visit(methodCall); |
| | | 144 | | // Continue from the collection the method was called on. For instance |
| | | 145 | | // methods Object is non-null; for static/extension methods (LINQ) the |
| | | 146 | | // first argument is the source. Methods with neither don't appear in |
| | | 147 | | // valid lambdas. |
| | 33 | 148 | | current = methodCall.Object ?? methodCall.Arguments[0]; |
| | | 149 | | } |
| | 1377 | 150 | | else if (current is MemberExpression memberExpr) |
| | | 151 | | { |
| | 744 | 152 | | current = memberExpr.Expression; |
| | | 153 | | } |
| | | 154 | | else |
| | | 155 | | { |
| | | 156 | | // Reached parameter or other expression type |
| | | 157 | | break; |
| | | 158 | | } |
| | | 159 | | } |
| | 633 | 160 | | } |
| | | 161 | | |
| | | 162 | | /// <summary> |
| | | 163 | | /// Visits method call expressions (e.g., LINQ methods like First, Where, Any). |
| | | 164 | | /// Tracks the base path for LINQ methods to properly resolve lambda parameter references. |
| | | 165 | | /// </summary> |
| | | 166 | | protected override Expression VisitMethodCall(MethodCallExpression node) |
| | | 167 | | { |
| | 153 | 168 | | var isLinqMethod = IsLinqMethod(node); |
| | 153 | 169 | | var basePath = isLinqMethod ? GetMethodCallBasePath(node) : null; |
| | | 170 | | |
| | 153 | 171 | | VisitReceiver(node); |
| | 153 | 172 | | VisitRemainingArguments(node, isLinqMethod, basePath); |
| | | 173 | | |
| | 153 | 174 | | return node; |
| | | 175 | | } |
| | | 176 | | |
| | | 177 | | private void VisitReceiver(MethodCallExpression node) |
| | | 178 | | { |
| | 153 | 179 | | if (node.Object is not null) |
| | | 180 | | { |
| | 69 | 181 | | Visit(node.Object); |
| | | 182 | | } |
| | 84 | 183 | | else if (node.Arguments.Count > 0) |
| | | 184 | | { |
| | 84 | 185 | | Visit(node.Arguments[0]); |
| | | 186 | | } |
| | 84 | 187 | | } |
| | | 188 | | |
| | | 189 | | private void VisitRemainingArguments(MethodCallExpression node, bool isLinqMethod, string? basePath) |
| | | 190 | | { |
| | 153 | 191 | | var startIndex = node.Object is not null ? 0 : 1; |
| | 540 | 192 | | for (var i = startIndex; i < node.Arguments.Count; i++) |
| | | 193 | | { |
| | 117 | 194 | | VisitArgument(node.Arguments[i], isLinqMethod, basePath); |
| | | 195 | | } |
| | 153 | 196 | | } |
| | | 197 | | |
| | | 198 | | private void VisitArgument(Expression arg, bool isLinqMethod, string? basePath) |
| | | 199 | | { |
| | 117 | 200 | | if (isLinqMethod && arg is LambdaExpression && !string.IsNullOrEmpty(basePath)) |
| | | 201 | | { |
| | 54 | 202 | | VisitWithLambdaContext(arg, basePath); |
| | 54 | 203 | | return; |
| | | 204 | | } |
| | 63 | 205 | | Visit(arg); |
| | 63 | 206 | | } |
| | | 207 | | |
| | | 208 | | private void VisitWithLambdaContext(Expression arg, string basePath) |
| | | 209 | | { |
| | 54 | 210 | | _lambdaContextPaths.Push(basePath); |
| | 108 | 211 | | try { Visit(arg); } |
| | 108 | 212 | | finally { _lambdaContextPaths.Pop(); } |
| | 54 | 213 | | } |
| | | 214 | | |
| | | 215 | | /// <summary> |
| | | 216 | | /// Determines if a method is a LINQ extension method from System.Linq. |
| | | 217 | | /// </summary> |
| | | 218 | | private static bool IsLinqMethod(MethodCallExpression node) |
| | | 219 | | { |
| | 153 | 220 | | var declaringType = node.Method.DeclaringType; |
| | 153 | 221 | | return declaringType is { Namespace: "System.Linq", Name: "Enumerable" or "Queryable" }; |
| | | 222 | | } |
| | | 223 | | |
| | | 224 | | /// <summary> |
| | | 225 | | /// Gets the base path from a LINQ method call (the collection being operated on). |
| | | 226 | | /// LINQ on Enumerable/Queryable is always an extension method whose first argument is |
| | | 227 | | /// the source collection — instance LINQ methods do not exist in the BCL, so |
| | | 228 | | /// node.Arguments is guaranteed non-empty here. |
| | | 229 | | /// </summary> |
| | | 230 | | private static string? GetMethodCallBasePath(MethodCallExpression node) |
| | 84 | 231 | | => BuildPathFromExpression(node.Arguments[0]); |
| | | 232 | | |
| | | 233 | | /// <summary> |
| | | 234 | | /// Builds a path from any expression (member or method chain). |
| | | 235 | | /// Lightweight version for determining base paths without marking as visited. |
| | | 236 | | /// </summary> |
| | 126 | 237 | | private static string? BuildPathFromExpression(Expression? expr) => expr switch |
| | 126 | 238 | | { |
| | 78 | 239 | | MemberExpression memberExpr => BuildPathFromMember(memberExpr), |
| | 39 | 240 | | MethodCallExpression methodCallExpr => BuildPathFromExpression(MethodCallSourceExpression(methodCallExpr)), |
| | 9 | 241 | | _ => null, |
| | 126 | 242 | | }; |
| | | 243 | | |
| | | 244 | | private static string? BuildPathFromMember(MemberExpression start) |
| | | 245 | | { |
| | | 246 | | // Loop entry adds at least one part; parts.Count is never 0 on exit. |
| | 78 | 247 | | var parts = new List<string>(); |
| | 435 | 248 | | for (MemberExpression? current = start; current is not null;) |
| | | 249 | | { |
| | 282 | 250 | | parts.Add(current.Member.Name); |
| | 285 | 251 | | if (!TryStepUp(current, parts, out current)) return null; |
| | | 252 | | } |
| | | 253 | | |
| | 75 | 254 | | parts.Reverse(); |
| | 75 | 255 | | return string.Join(".", parts); |
| | | 256 | | } |
| | | 257 | | |
| | | 258 | | // Returns true if the chain continues (next set, possibly null when terminating at a parameter |
| | | 259 | | // or a successfully-resolved method call); returns false to signal an invalid chain shape. |
| | | 260 | | private static bool TryStepUp(MemberExpression current, List<string> parts, out MemberExpression? next) |
| | | 261 | | { |
| | 282 | 262 | | switch (current.Expression) |
| | | 263 | | { |
| | | 264 | | case MemberExpression nextMember: |
| | 204 | 265 | | next = nextMember; |
| | 204 | 266 | | return true; |
| | | 267 | | case ParameterExpression: |
| | 72 | 268 | | next = null; |
| | 72 | 269 | | return true; |
| | | 270 | | case MethodCallExpression methodCall: |
| | 3 | 271 | | var basePath = BuildPathFromExpression(MethodCallSourceExpression(methodCall)); |
| | 6 | 272 | | if (basePath is not null) parts.Add(basePath); |
| | 3 | 273 | | next = null; |
| | 3 | 274 | | return true; |
| | | 275 | | default: |
| | 3 | 276 | | next = null; |
| | 3 | 277 | | return false; |
| | | 278 | | } |
| | | 279 | | } |
| | | 280 | | |
| | | 281 | | /// <summary> |
| | | 282 | | /// Visits lambda expressions (e.g., predicates in First, Where). |
| | | 283 | | /// </summary> |
| | | 284 | | protected override Expression VisitLambda<T>(Expression<T> node) |
| | | 285 | | { |
| | | 286 | | // Track the root parameter for the outermost lambda (not nested LINQ lambdas) |
| | 486 | 287 | | if (_rootParameter == null && _lambdaContextPaths.Count == 0 && node.Parameters.Count > 0) |
| | | 288 | | { |
| | 417 | 289 | | _rootParameter = node.Parameters[0]; |
| | | 290 | | // Track ALL parameter types for multi-parameter lambdas |
| | 1746 | 291 | | foreach (var param in node.Parameters) |
| | | 292 | | { |
| | 456 | 293 | | _rootParameterTypes.Add(param.Type); |
| | | 294 | | } |
| | | 295 | | } |
| | | 296 | | |
| | | 297 | | // Visit the body to extract paths from nested predicates |
| | | 298 | | // Example: items.First(p => p.sport == "F") |
| | 486 | 299 | | Visit(node.Body); |
| | 486 | 300 | | return node; |
| | | 301 | | } |
| | | 302 | | |
| | | 303 | | /// <summary> |
| | | 304 | | /// Visits parameter expressions (e.g., direct parameter references like null checks). |
| | | 305 | | /// </summary> |
| | | 306 | | protected override Expression VisitParameter(ParameterExpression node) |
| | | 307 | | { |
| | | 308 | | // If this is the root parameter being used directly (not in a nested lambda context), |
| | | 309 | | // add it as a field path. This handles cases like: playerProfile => playerProfile == null |
| | | 310 | | // Lambdas authored in C# always carry a parameter name; the BCL's |
| | | 311 | | // Expression.Parameter overloads accept null but no public path produces such |
| | | 312 | | // parameters here, so we treat the name as non-null. |
| | 45 | 313 | | if (_rootParameter != null && node == _rootParameter && _lambdaContextPaths.Count == 0) |
| | | 314 | | { |
| | 33 | 315 | | FieldPaths.Add(node.Name!); |
| | | 316 | | } |
| | | 317 | | |
| | 45 | 318 | | return node; |
| | | 319 | | } |
| | | 320 | | |
| | | 321 | | /// <summary> |
| | | 322 | | /// Visits binary expressions (e.g., comparisons like greater than, equals, and logical operators). |
| | | 323 | | /// </summary> |
| | | 324 | | protected override Expression VisitBinary(BinaryExpression node) |
| | | 325 | | { |
| | | 326 | | // Visit both sides of the binary expression |
| | 633 | 327 | | Visit(node.Left); |
| | 633 | 328 | | Visit(node.Right); |
| | 633 | 329 | | return node; |
| | | 330 | | } |
| | | 331 | | |
| | | 332 | | /// <summary> |
| | | 333 | | /// Visits unary expressions (e.g., !, conversions). |
| | | 334 | | /// </summary> |
| | | 335 | | protected override Expression VisitUnary(UnaryExpression node) |
| | | 336 | | { |
| | 48 | 337 | | Visit(node.Operand); |
| | 48 | 338 | | return node; |
| | | 339 | | } |
| | | 340 | | |
| | | 341 | | /// <summary> |
| | | 342 | | /// Visits new expressions (e.g., anonymous type creation). |
| | | 343 | | /// </summary> |
| | | 344 | | protected override Expression VisitNew(NewExpression node) |
| | | 345 | | { |
| | | 346 | | // Visit all constructor arguments to extract paths |
| | | 347 | | // Handles: new { x.user.name, x.user.email } |
| | 84 | 348 | | foreach (var arg in node.Arguments) |
| | | 349 | | { |
| | 24 | 350 | | Visit(arg); |
| | | 351 | | } |
| | | 352 | | |
| | 18 | 353 | | return node; |
| | | 354 | | } |
| | | 355 | | |
| | | 356 | | /// <summary> |
| | | 357 | | /// Visits member initialization expressions (e.g., object initializers). |
| | | 358 | | /// </summary> |
| | | 359 | | protected override Expression VisitMemberInit(MemberInitExpression node) |
| | | 360 | | { |
| | 3 | 361 | | Visit(node.NewExpression); |
| | | 362 | | |
| | 12 | 363 | | foreach (var binding in node.Bindings) |
| | | 364 | | { |
| | 3 | 365 | | if (binding is MemberAssignment assignment) |
| | | 366 | | { |
| | 3 | 367 | | Visit(assignment.Expression); |
| | | 368 | | } |
| | | 369 | | } |
| | | 370 | | |
| | 3 | 371 | | return node; |
| | | 372 | | } |
| | | 373 | | |
| | | 374 | | /// <summary> |
| | | 375 | | /// Visits conditional expressions (e.g., ternary operator). |
| | | 376 | | /// </summary> |
| | | 377 | | protected override Expression VisitConditional(ConditionalExpression node) |
| | | 378 | | { |
| | 18 | 379 | | Visit(node.Test); |
| | 18 | 380 | | Visit(node.IfTrue); |
| | 18 | 381 | | Visit(node.IfFalse); |
| | 18 | 382 | | return node; |
| | | 383 | | } |
| | | 384 | | |
| | | 385 | | /// <summary> |
| | | 386 | | /// Builds a dot-separated field path from a member expression chain. |
| | | 387 | | /// Handles LINQ method calls like First(), Where(), etc. |
| | | 388 | | /// </summary> |
| | | 389 | | /// <param name="node">The member expression to analyze</param> |
| | | 390 | | /// <returns>The dot-separated path (e.g., "user.profile.age") or null if not a valid path</returns> |
| | | 391 | | private string? BuildMemberPath(MemberExpression node) |
| | | 392 | | { |
| | | 393 | | // The first StepUpChain call always pushes node.Member.Name onto parts before |
| | | 394 | | // it can fail or terminate, so parts is non-empty by the time we exit the loop. |
| | 633 | 395 | | var parts = new Stack<string>(); |
| | 633 | 396 | | Expression? currentExpr = node; |
| | 633 | 397 | | ParameterExpression? parameterExpr = null; |
| | | 398 | | |
| | 2067 | 399 | | while (currentExpr != null) |
| | | 400 | | { |
| | 2067 | 401 | | var step = StepUpChain(currentExpr, parts); |
| | 2067 | 402 | | if (step.IsTerminal) |
| | | 403 | | { |
| | 633 | 404 | | parameterExpr = step.Parameter; |
| | 639 | 405 | | if (!step.Valid) return null; |
| | | 406 | | break; |
| | | 407 | | } |
| | 1434 | 408 | | currentExpr = step.Next; |
| | | 409 | | } |
| | | 410 | | |
| | | 411 | | // For multi-parameter lambdas (more than 1 root parameter type), include parameter name |
| | 627 | 412 | | if (_rootParameterTypes.Count > 1 && parameterExpr != null && !string.IsNullOrEmpty(parameterExpr.Name)) |
| | | 413 | | { |
| | 30 | 414 | | parts.Push(parameterExpr.Name); |
| | | 415 | | } |
| | | 416 | | |
| | 627 | 417 | | return string.Join(".", parts); |
| | | 418 | | } |
| | | 419 | | |
| | | 420 | | /// <summary> |
| | | 421 | | /// One step of <see cref="BuildMemberPath"/>'s chain walk. Returns either the next |
| | | 422 | | /// expression to walk (<see cref="Next"/>) or a terminal status (<see cref="IsTerminal"/>) |
| | | 423 | | /// indicating whether the chain ended cleanly at a parameter (<see cref="Valid"/>) or hit |
| | | 424 | | /// an unsupported node shape. |
| | | 425 | | /// </summary> |
| | | 426 | | private readonly ref struct ChainStep |
| | | 427 | | { |
| | 2868 | 428 | | public Expression? Next { get; init; } |
| | 1260 | 429 | | public ParameterExpression? Parameter { get; init; } |
| | 2700 | 430 | | public bool IsTerminal { get; init; } |
| | 1266 | 431 | | public bool Valid { get; init; } |
| | | 432 | | |
| | 1434 | 433 | | public static ChainStep Continue(Expression? next) => new() { Next = next }; |
| | 627 | 434 | | public static ChainStep TerminateAt(ParameterExpression p) => new() { IsTerminal = true, Valid = true, Param |
| | 6 | 435 | | public static ChainStep Invalid() => new() { IsTerminal = true, Valid = false }; |
| | | 436 | | } |
| | | 437 | | |
| | | 438 | | private static ChainStep StepUpChain(Expression currentExpr, Stack<string> parts) |
| | | 439 | | { |
| | | 440 | | switch (currentExpr) |
| | | 441 | | { |
| | | 442 | | case MemberExpression memberExpr: |
| | 1389 | 443 | | parts.Push(memberExpr.Member.Name); |
| | 1389 | 444 | | return ChainStep.Continue(memberExpr.Expression); |
| | | 445 | | |
| | | 446 | | case MethodCallExpression methodCall: |
| | 33 | 447 | | return ChainStep.Continue(MethodCallSourceExpression(methodCall)); |
| | | 448 | | |
| | | 449 | | case BinaryExpression { NodeType: ExpressionType.Coalesce } binaryExpr: |
| | 6 | 450 | | return ChainStep.Continue(binaryExpr.Left); |
| | | 451 | | |
| | | 452 | | case ConditionalExpression conditionalExpr: |
| | 6 | 453 | | return ChainStep.Continue(SelectConditionalBranch(conditionalExpr)); |
| | | 454 | | |
| | | 455 | | case ParameterExpression paramExpr: |
| | 627 | 456 | | return ChainStep.TerminateAt(paramExpr); |
| | | 457 | | |
| | | 458 | | default: |
| | 6 | 459 | | return ChainStep.Invalid(); |
| | | 460 | | } |
| | | 461 | | } |
| | | 462 | | |
| | | 463 | | // Either methodCall.Object is non-null (instance method) or Arguments has at least one |
| | | 464 | | // entry (the source for an extension/static method); valid lambdas don't produce both |
| | | 465 | | // null Object and zero arguments. |
| | | 466 | | private static Expression? MethodCallSourceExpression(MethodCallExpression methodCall) |
| | 75 | 467 | | => methodCall.Object ?? methodCall.Arguments[0]; |
| | | 468 | | |
| | | 469 | | /// <summary> |
| | | 470 | | /// Picks the branch of a null-conditional-style ternary that carries member access. |
| | | 471 | | /// IfTrue takes precedence; the IfFalse branch wins only when IfTrue is the constant null. |
| | | 472 | | /// </summary> |
| | | 473 | | private static Expression SelectConditionalBranch(ConditionalExpression conditionalExpr) |
| | 6 | 474 | | => conditionalExpr.IfTrue is ConstantExpression { Value: null } |
| | 6 | 475 | | ? conditionalExpr.IfFalse |
| | 6 | 476 | | : conditionalExpr.IfTrue; |
| | | 477 | | |
| | | 478 | | /// <summary> |
| | | 479 | | /// Determines if a member expression represents a property that should be excluded. |
| | | 480 | | /// Excludes only System.* types (like string.Length); IL-emitted properties from |
| | | 481 | | /// QueryBuilderTypeGenerator and user types are kept. |
| | | 482 | | /// </summary> |
| | | 483 | | private static bool ShouldExcludeProperty(MemberExpression memberExpr) |
| | 675 | 484 | | => memberExpr.Member.DeclaringType!.Namespace?.StartsWith("System") == true; |
| | | 485 | | } |
| | | 486 | | } |