Summary
Constructor translations in the SQL translating expression visitor (e.g. new DateTimeOffset(dateTime, offset) → TODATETIMEOFFSET(datetime, offset)) do not work inside Select() projections. They only work in Where(), OrderBy(), GroupBy(), etc.
This is because RelationalProjectionBindingExpressionVisitor.Visit() routes NewExpression directly to base.Visit() → VisitNew(), which performs client-side DTO/anonymous type construction, rather than routing it through TranslateProjection() which would invoke the SQL translator's VisitNew override.
Minimal repro
// This WORKS (Where):
var results = await ctx.BasicTypes
.Where(b => new DateTimeOffset(b.DateTime, new TimeSpan(2, 0, 0)) == someValue)
.ToListAsync();
// This DOES NOT WORK (Select) - the constructor is evaluated client-side instead of being translated:
var results = await ctx.BasicTypes
.Select(b => new DateTimeOffset(b.DateTime, new TimeSpan(2, 0, 0)))
.ToListAsync();
Details
In RelationalProjectionBindingExpressionVisitor.Visit(), NewExpression is always routed to base.Visit() which calls VisitNew() for client-side construction:
case NewExpression or MemberInitExpression or StructuralTypeShaperExpression or IncludeExpression:
return base.Visit(expression);
To support constructor translation in projections, NewExpression in index-based binding mode needs to be routed through TranslateProjection() (similar to how other expressions are handled). However, this must be done carefully to avoid breaking existing scenarios like new List<int> { ... } collection initializers, which use ListInitExpression containing a NewExpression.
A naive fix of routing all NewExpression through TranslateProjection in index-based binding mode breaks tests like GearsOfWarQuerySqlServerTest.Optional_navigation_type_compensation_works_with_list_initializers.
Summary
Constructor translations in the SQL translating expression visitor (e.g.
new DateTimeOffset(dateTime, offset)→TODATETIMEOFFSET(datetime, offset)) do not work insideSelect()projections. They only work inWhere(),OrderBy(),GroupBy(), etc.This is because
RelationalProjectionBindingExpressionVisitor.Visit()routesNewExpressiondirectly tobase.Visit()→VisitNew(), which performs client-side DTO/anonymous type construction, rather than routing it throughTranslateProjection()which would invoke the SQL translator'sVisitNewoverride.Minimal repro
Details
In
RelationalProjectionBindingExpressionVisitor.Visit(),NewExpressionis always routed tobase.Visit()which callsVisitNew()for client-side construction:To support constructor translation in projections,
NewExpressionin index-based binding mode needs to be routed throughTranslateProjection()(similar to how other expressions are handled). However, this must be done carefully to avoid breaking existing scenarios likenew List<int> { ... }collection initializers, which useListInitExpressioncontaining aNewExpression.A naive fix of routing all
NewExpressionthroughTranslateProjectionin index-based binding mode breaks tests likeGearsOfWarQuerySqlServerTest.Optional_navigation_type_compensation_works_with_list_initializers.