Fix Expression constant folding inside of MemberInitExpression#5298
Fix Expression constant folding inside of MemberInitExpression#5298MichalMarsalek wants to merge 9 commits into
MemberInitExpression#5298Conversation
|
Hi @MichalMarsalek, thank you for the contribution! This PR has been idle for ~285 days. Is this change still needed on your side? Flagging for SDK-team triage as part of an open-PR cleanup pass — we'd like to give you a clear yes/no rather than let it sit. |
7aae750 to
0dd36a3
Compare
|
Azure Pipelines: 2 pipeline(s) require an authorized user to comment /azp run to run. |
|
Hi @NaluTripician. I just rebased my changes and yes, it seems that the problem they are addressing still exist. I think the change is still needed. |
|
@sdkReviewAgent |
NaluTripician
left a comment
There was a problem hiding this comment.
Deep Review — Fix Expression constant folding inside MemberInitExpression
Verdict: Net-positive bug fix. No blockers (0 critical, 0 major). The fix correctly recurses into MemberInit bindings so closure-captured variables are folded.
The deliberate choice to not visit node.NewExpression is load-bearing — see the inline comment on SubtreeEvaluator.cs for the explanation. This is correct, not a defect.
Answering your question in the PR description
"vast majority of the expressions defining the document projections have the
MemberInitExpressionas the very root of the body … no constant folding would ever happen. Is that even possible?"
You aren't missing anything — the original 2018 implementation simply got this wrong, and the existing LinqTranslationBaselineTests.TestMemberInitializer baseline only exercises member access on the lambda parameter (doc.NumericField), not on captured locals. Anonymous-type projections use NewExpression and have always folded correctly, which is why this only surfaced in the relatively rare class-with-member-init case from #1664.
Observation — design layering (not in scope, worth a follow-up issue)
The Nominator nominates expressions bottom-up using the full default ExpressionVisitor, so it freely adds children of MemberInitExpression (including the inner NewExpression) to the candidate set. But SubtreeEvaluator cannot safely evaluate those candidates — folding a NewExpression inside a MemberInit would break the parent. The current workaround is "in SubtreeEvaluator, manually opt out of visiting paths that would lead us to fold something the parent can't accept." A more principled design would either (a) have the Nominator understand parent constraints, or (b) make EvaluateConstant return the original expression unchanged when the result would violate a structural constraint of the consumer. Worth filing a tracking issue; the customer-visible fix here is the right next step.
Findings overview
All inline comments are 🟡 Recommendations or 🟢 Suggestions — mostly test-coverage gaps. Please consider addressing #1, #2, #4, #5 before merge.
SubtreeEvaluator.cs - Add load-bearing intent comment in VisitMemberInit explaining why node.NewExpression is intentionally NOT visited (parameterless ew T() would be nominated and folded to a ConstantExpression, breaking the Expression.MemberInit contract). - Short-circuit no-change case using the protected static ExpressionVisitor.Visit<T>(...) helper to avoid allocating a new MemberInitExpression on every LINQ query when nothing folded. ConstantEvaluatorTests.cs - Trim unused using directives (8 of 12 were unused template scaffolding). - Split the single ClosuresAreEvaluated test into per-scenario test methods so a regression names the actual broken shape. - Replace fragile Expression.ToString() string-equality assertions with structural shape assertions (MemberInit -> MemberAssignment -> Binary ...) via small typed helpers. - Add coverage for shapes the fix newly affects but the original test missed: dictionary indexer keyed by a closure variable (the exact shape from issue Azure#1664), nested MemberInit, MemberMemberBinding ( ew Outer { Inner = { ... } }), and MemberListBinding ( ew Outer { Items = { ... } }). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Re-review after — ready to merge from review-feedback perspectiveI pushed `` directly to What changed since the previous review pass
Verification done locally
Deferred — needs follow-up issue (not blocking this PR)
Remaining merge gates (out of scope for review feedback)
Net: from a review-feedback perspective the PR is in good shape to merge once CI is green. |
|
/azp run |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
Confirmed the unit tests in this PR pass locally against the PR head ( The two failing CI jobs ( |
NaluTripician
left a comment
There was a problem hiding this comment.
Approved, verified live tests pass locally until fork PR issue is fixed
kushagraThapar
left a comment
There was a problem hiding this comment.
PR looks good, request changes on the changelog entry.
… fix Address @kushagraThapar review feedback requesting a changelog entry for PR Azure#5298. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
@kushagraThapar — added a changelog entry under a new
Format matches the existing |
# Conflicts: # changelog.md
# Conflicts: # changelog.md
Resolves changelog.md Unreleased Bugs Fixed section: keeps both the Azure#5298 LINQ MemberInit fix entry and the Azure#5870 CrossRegionHedgingAvailabilityStrategy entry from main. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
/azp run |
|
Azure Pipelines successfully started running 1 pipeline(s). |
kushagraThapar
left a comment
There was a problem hiding this comment.
LGTM, thanks @MichalMarsalek !
# Conflicts: # changelog.md
|
/azp run |
|
Azure Pipelines successfully started running 1 pipeline(s). |
Pull Request Template
Description
This PR fixes a bug described in #1664 where captured variables in LINQ expressions would not get expanded leading to a
{"testName": "Test"}["testName"]expression (instead of the expected"Test") in the translated SQL, leading to an error reported by Cosmos.However the problem appears to be more general as the issue seems to be that the recursion that partially evaluates expressions (so among other things resolves the captured variables and therefore effectively changes
{"testName": "Test"}["testName"]to"Test") terminates whenever it encounters a node of typeMemberInitExpression. This explains the difference in behaviour between anonymous types and a class as described in #1664, as anonymous types do not contain theMemberInitExpressionnode.This is quite surprising since at least in the way I am used to using this library, vast majority of the expressions defining the document projections have the
MemberInitExpressionas the very root of the body of theExpression<Func<T1, T2>>, meaning that in this common case no constant folding would ever happen. Is that even possible? Or what am I missing here?Type of change
Closing issues
To automatically close an issue: closes #1664