@@ -341,15 +341,14 @@ private Expression _AddProjectableSelect(Expression node, IEntityType entityType
341341 return node ;
342342 }
343343
344- var properties = entityType . GetProperties ( )
344+ var members = entityType . GetProperties ( )
345345 . Where ( x => ! x . IsShadowProperty ( ) )
346- . Select ( x => x . GetMemberInfo ( false , false ) )
346+ . Cast < IPropertyBase > ( )
347347 . Concat ( entityType . GetNavigations ( )
348- . Where ( x => ! x . IsShadowProperty ( ) )
349- . Select ( x => x . GetMemberInfo ( false , false ) ) )
348+ . Where ( x => ! x . IsShadowProperty ( ) ) )
350349 . Concat ( entityType . GetSkipNavigations ( )
351- . Where ( x => ! x . IsShadowProperty ( ) )
352- . Select ( x => x . GetMemberInfo ( false , false ) ) )
350+ . Where ( x => ! x . IsShadowProperty ( ) ) )
351+ . Select ( x => x . GetMemberInfo ( false , false ) )
353352 // Remove projectable properties from the ef properties. Since properties returned here for auto
354353 // properties (like `public string Test {get;set;}`) are generated fields, we also need to take them into account.
355354 . Where ( x => projectableProperties . All ( y => x . Name != y . Name && x . Name != $ "<{ y . Name } >k__BackingField") ) ;
@@ -364,7 +363,7 @@ private Expression _AddProjectableSelect(Expression node, IEntityType entityType
364363 Expression . Lambda (
365364 Expression . MemberInit (
366365 Expression . New ( entityType . ClrType ) ,
367- properties . Select ( x => Expression . Bind ( x , Expression . MakeMemberAccess ( xParam , x ) ) )
366+ members . Select ( x => _MakeBinding ( xParam , x ) )
368367 . Concat ( projectableProperties
369368 . Select ( x => Expression . Bind ( x , _GetAccessor ( x , xParam ) ) )
370369 )
@@ -374,6 +373,59 @@ private Expression _AddProjectableSelect(Expression node, IEntityType entityType
374373 ) ;
375374 }
376375
376+ // Builds the member binding used to copy an EF-mapped member into the re-projected entity.
377+ //
378+ // EF's member metadata resolves auto-properties to their compiler-generated backing field.
379+ // Reading a mapped scalar or navigation through that backing field prevents EF's navigation
380+ // expansion from recognizing it as a plain column/navigation, so it instead materializes the
381+ // whole source entity — including any Include-ed collection navigations — once for every
382+ // rewritten member. That produces one duplicate JOIN per member (see issue #217).
383+ //
384+ // Reading through the CLR property lets EF treat scalars as columns and collapse a collection
385+ // navigation into a single join. When the property is also settable we bind to the property
386+ // itself so the generated projection keeps the natural member/column names instead of the
387+ // backing-field name.
388+ private static MemberAssignment _MakeBinding ( ParameterExpression para , MemberInfo member )
389+ {
390+ var property = _ResolveClrProperty ( member ) ;
391+ if ( property is not null && property . CanRead )
392+ {
393+ var read = Expression . MakeMemberAccess ( para , property ) ;
394+ return property . CanWrite
395+ ? Expression . Bind ( property , read )
396+ : Expression . Bind ( member , read ) ;
397+ }
398+
399+ return Expression . Bind ( member , Expression . MakeMemberAccess ( para , member ) ) ;
400+ }
401+
402+ // Resolves the CLR property backing an EF member, whether EF handed us the property directly
403+ // or the compiler-generated "<Name>k__BackingField" for an auto-property.
404+ private static PropertyInfo ? _ResolveClrProperty ( MemberInfo member )
405+ {
406+ switch ( member )
407+ {
408+ case PropertyInfo property :
409+ return property ;
410+ case FieldInfo { Name : [ '<' , ..] } field when field . DeclaringType is { } declaringType :
411+ var closingBracket = field . Name . IndexOf ( '>' ) ;
412+ if ( closingBracket > 1 )
413+ {
414+ var propertyName = field . Name . Substring ( 1 , closingBracket - 1 ) ;
415+ var property = declaringType . GetProperty ( propertyName ,
416+ BindingFlags . Public | BindingFlags . NonPublic | BindingFlags . Instance ) ;
417+ if ( property is not null && property . PropertyType == field . FieldType )
418+ {
419+ return property ;
420+ }
421+ }
422+
423+ return null ;
424+ default :
425+ return null ;
426+ }
427+ }
428+
377429 private Expression _GetAccessor ( PropertyInfo property , ParameterExpression para )
378430 {
379431 var lambda = _resolver . FindGeneratedExpression ( property ) ;
0 commit comments