@@ -236,6 +236,21 @@ private Expression VisitSelectManyCall(MethodCallExpression m)
236236 var retVal = Visit ( m . Arguments [ 0 ] ) ;
237237 Context . LeftJoinAlias = leftJoinAlias ;
238238
239+ // Bind the result-selector parameters to their backing aliases so
240+ // downstream visitors (and any sub-query that inherits Aliases)
241+ // can resolve `<transparentId>.<field>` and `<rightSide>` chains.
242+ // First param projects onto the running outer scope (FROM table or
243+ // previous transparent identifier); second param maps to the LEFT
244+ // JOIN alias produced by the source GroupJoin.
245+ if ( resultSelector . Parameters . Count > 0 )
246+ {
247+ var outerAlias = Context . TableAlias . IsEmpty ( ) ? Extensions . DefaultAlias : Context . TableAlias ;
248+ Context . Aliases [ resultSelector . Parameters [ 0 ] ] = outerAlias ;
249+ }
250+
251+ if ( resultSelector . Parameters . Count > 1 )
252+ Context . Aliases [ resultSelector . Parameters [ 1 ] ] = resultSelector . Parameters [ 1 ] . Name ;
253+
239254 if ( resultSelector . Body is MemberInitExpression )
240255 {
241256 // final projection folded into SelectMany result selector
@@ -527,42 +542,32 @@ private Expression ProcessInitExpression(Expression maExp)
527542 Curr . OpenBracket ( ) ;
528543 Curr . NewLine ( ) ;
529544
530- Context = new ( ) { ParamCountOffset = ctx . Parameters . Count + ctx . ParamCountOffset } ;
531-
532- if ( visitor ? . GetType ( ) . GetGenericType ( typeof ( CountVisitor < > ) ) != null )
533- Context . Count = true ;
534- }
535-
536- Visit ( maExp ) ;
545+ // Pre-analyse the sub-query so the element type and the
546+ // lambda-parameter alias are known BEFORE Visit. Setting
547+ // TableAlias up front lets RegisterLambdaParameters bind the
548+ // inner lambda's parameter to the right alias instead of the
549+ // default one — without this the inner WHERE references "e"
550+ // while FROM declares "x" and the SQL fails to bind.
551+ AnalyseSubqueryShape ( maExp , out var subItemType , out var subAlias ) ;
537552
538- if ( isSubquery )
539- {
540- Type itemType ;
541-
542- if ( maExp is MethodCallExpression mca )
553+ Context = new ( )
543554 {
544- var args = mca . Method . GetGenericArguments ( ) ;
545-
546- itemType = args . Length > 0 ? args [ 0 ] : typeof ( int ) ;
547-
548- while ( mca . Arguments [ 0 ] is MethodCallExpression mca1 )
549- mca = mca1 ;
555+ ParamCountOffset = ctx . Parameters . Count + ctx . ParamCountOffset ,
556+ TableAlias = subAlias ,
557+ } ;
550558
551- Context . TableAlias = ( mca . Arguments [ 1 ] is MemberExpression
552- ? mca . Arguments [ 2 ]
553- : mca . Arguments [ 1 ] )
554- . GetOperand ( ) . Parameters [ 0 ] . Name ;
559+ // Inherit outer alias bindings so a correlated reference like
560+ // `outerParam.Field` inside the sub-query resolves to the outer
561+ // FROM alias instead of leaking the transparent-id name.
562+ foreach ( var kv in ctx . Aliases )
563+ Context . Aliases [ kv . Key ] = kv . Value ;
555564
556- if ( itemType . IsSerializablePrimitive ( ) )
557- {
558- itemType = ( ( MemberExpression ) mca . Arguments [ 0 ] ) . Member . GetMemberType ( ) . GetGenericArguments ( ) [ 0 ] ;
559- }
560- }
561- else
562- itemType = _meta . EntityType ;
565+ if ( visitor ? . GetType ( ) . GetGenericType ( typeof ( CountVisitor < > ) ) != null )
566+ Context . Count = true ;
563567
564- var subquery = Context . Build ( SchemaRegistry . Get ( itemType ) ) ;
568+ Visit ( maExp ) ;
565569
570+ var subquery = Context . Build ( SchemaRegistry . Get ( subItemType ?? _meta . EntityType ) ) ;
566571 var subParams = Context . Parameters ;
567572
568573 Context = ctx ;
@@ -571,10 +576,43 @@ private Expression ProcessInitExpression(Expression maExp)
571576
572577 Context . AddParamsFromSubquery ( subParams , false ) ;
573578 }
579+ else
580+ {
581+ Visit ( maExp ) ;
582+ }
574583
575584 return maExp ;
576585 }
577586
587+ /// <summary>
588+ /// Walks the outermost LINQ chain in <paramref name="maExp"/> to extract
589+ /// (a) the element type of the sub-query result and (b) the lambda
590+ /// parameter name of the deepest source. Both are needed to set up the
591+ /// sub-query <see cref="Context"/> before the visitor descends.
592+ /// </summary>
593+ private static void AnalyseSubqueryShape ( Expression maExp , out Type itemType , out string alias )
594+ {
595+ itemType = null ;
596+ alias = null ;
597+
598+ if ( maExp is not MethodCallExpression mca )
599+ return ;
600+
601+ var args = mca . Method . GetGenericArguments ( ) ;
602+ itemType = args . Length > 0 ? args [ 0 ] : typeof ( int ) ;
603+
604+ while ( mca . Arguments [ 0 ] is MethodCallExpression mca1 )
605+ mca = mca1 ;
606+
607+ alias = ( mca . Arguments [ 1 ] is MemberExpression
608+ ? mca . Arguments [ 2 ]
609+ : mca . Arguments [ 1 ] )
610+ . GetOperand ( ) . Parameters [ 0 ] . Name ;
611+
612+ if ( itemType . IsSerializablePrimitive ( ) )
613+ itemType = ( ( MemberExpression ) mca . Arguments [ 0 ] ) . Member . GetMemberType ( ) . GetGenericArguments ( ) [ 0 ] ;
614+ }
615+
578616 protected override Expression VisitMemberInit ( MemberInitExpression i )
579617 {
580618 var selectColumns = new ContextSelectColumns ( ) ;
@@ -1069,14 +1107,19 @@ private string GetAlias(string alias)
10691107 /// <summary>
10701108 /// Lookup overload that resolves a <see cref="ParameterExpression"/>
10711109 /// directly through <see cref="Context.Aliases"/> when the parameter
1072- /// has been registered by the enclosing LINQ-method handler. Falls
1073- /// back to the string-based <see cref="GetAlias(string)"/> otherwise
1074- /// so callers do not need to know whether registration happened.
1110+ /// has been registered by the enclosing LINQ-method handler. The
1111+ /// stored value is already the canonical alias and must NOT be piped
1112+ /// through the string-based fallback — that would remap every alias
1113+ /// to <see cref="Context.TableAlias"/> and erase outer-scope
1114+ /// references in sub-queries that inherit the parent's alias map.
1115+ /// Falls back to the string-based <see cref="GetAlias(string)"/> when
1116+ /// the parameter is not registered, so callers do not need to know
1117+ /// whether registration happened.
10751118 /// </summary>
10761119 private string GetAlias ( ParameterExpression parameter )
10771120 {
10781121 if ( parameter is not null && Context . Aliases . TryGetValue ( parameter , out var alias ) )
1079- return GetAlias ( alias ) ;
1122+ return alias ;
10801123
10811124 return GetAlias ( parameter ? . Name ) ;
10821125 }
@@ -1170,10 +1213,13 @@ protected override Expression VisitMember(MemberExpression m)
11701213
11711214 if ( ! TryEmitFromResolver ( m . Update ( me ) ) )
11721215 {
1173- // Last-resort fallback: emit through the legacy alias-name
1174- // derivation so cases the resolver does not yet recognise
1175- // continue to work as before.
1176- Curr . Column ( me . Member . Name , m . Member . Name ) ;
1216+ // Walk the transparent-id chain to the actual lambda
1217+ // parameter and use its registered alias — using
1218+ // <c>me.Member.Name</c> (the field name on the
1219+ // compiler-generated transparent identifier) leaks as
1220+ // a fake SQL alias and breaks any chain longer than
1221+ // one hop.
1222+ Curr . Column ( ResolveTransIdAlias ( me ) , m . Member . Name ) ;
11771223 }
11781224 }
11791225 }
@@ -1459,9 +1505,17 @@ void AddColumnFromMember(MemberExpression member)
14591505 /// </summary>
14601506 private void EmitMaterialisedValue ( MemberInfo leafMember , object value )
14611507 {
1462- if ( value is IQueryable q && q . Expression is not ConstantExpression )
1508+ if ( value is IQueryable q )
14631509 {
1464- Visit ( q . Expression ) ;
1510+ // Captured local IQueryable<T>: re-visit the wrapped expression
1511+ // when it carries query operators, otherwise emit nothing.
1512+ // Constant-rooted queryables (the common Query<T>() shape)
1513+ // represent the bare table — the surrounding sub-query handler
1514+ // resolves the element type to a FROM, so a value-emit here
1515+ // would corrupt the sub-query's WHERE with a non-bindable
1516+ // IQueryable parameter.
1517+ if ( q . Expression is not ConstantExpression )
1518+ Visit ( q . Expression ) ;
14651519 return ;
14661520 }
14671521
0 commit comments