Skip to content

Optimizable expressions#83

Open
georghinkel wants to merge 19 commits intomainfrom
optimizable-expressions
Open

Optimizable expressions#83
georghinkel wants to merge 19 commits intomainfrom
optimizable-expressions

Conversation

@georghinkel
Copy link
Copy Markdown
Contributor

@georghinkel georghinkel commented Jan 20, 2025

Summary by MergeMonkey

  • Docs & Guides:
    • Added query optimization interfaces and implementations for expression classes
  • Fresh Additions:
    • Projection merging for query optimization - combines consecutive Select/SelectMany/Join/GroupJoin expressions into single expressions
    • Optimizable expression extensions enabling query optimizer to chain and merge projections
    • Default query optimizer with static accessor for runtime configuration

@georghinkel georghinkel self-assigned this Feb 5, 2025
@georghinkel georghinkel marked this pull request as draft February 12, 2025 07:57
@mergemonkeyhq
Copy link
Copy Markdown

mergemonkeyhq bot commented Jan 16, 2026

Risk AssessmentNEEDS-TESTING · ~25 min review

Focus areas: Query optimizer recursion safety · Expression merging correctness · Unused imports and parameters · Test coverage for edge cases

Assessment: New query optimization feature that modifies expression evaluation behavior and could affect correctness of incremental computations

Walkthrough

The PR adds query optimization infrastructure to the NMF Expressions library. When AsNotifiable() is called on a SelectExpression, it now runs the query optimizer which attempts to merge consecutive projection expressions (Select/SelectMany/Join/GroupJoin) into single expressions. The optimizer tracks previous expressions via PrevExpression property and uses ProjectionMergeQueryOptimizer to combine lambda expressions, reducing the number of enumeration passes over the source data. New test methods verify that optimized queries still produce correct incremental change notifications.

Changes

Files Summary
Query Optimization Infrastructure
Expressions/Expressions.Linq/ExpressionsOptimization/IQueryOptimizer.cs, ProjectionMergeQueryOptimizer.cs, QueryOptimizer.cs, QueryOptimizerVisitor.cs, OptimizableExpressionExtensions.cs
Implements query optimization framework with projection merging to combine consecutive Select/SelectMany/Join/GroupJoin expressions into single optimized expressions, reducing redundant enumeration overhead.
Expression Interface Extensions
Expressions/Expressions.Linq/Facade/IEnumerableExpression.cs
Adds ISQO and IOptimizableEnumerableExpression interfaces with generic variants to enable expression classes to participate in query optimization chain.
Select Expression Optimization
Expressions/Expressions.Linq/EnumerableExpressions/SelectExpression.cs
Implements IOptimizableEnumerableExpression TResult, adds OptSelectorExpression/PrevExpression/OptSource properties, integrates optimizer in AsNotifiable method, adds AsOptimized/Merge methods for projection merging.
SelectMany Expression Optimization
Expressions/Expressions.Linq/EnumerableExpressions/SelectManyExpression.cs
Implements IOptimizableEnumerableExpression TResult, adds optimization properties and AsOptimized/Merge methods to enable projection merging with previous expressions.
Join Expression Optimization
Expressions/Expressions.Linq/EnumerableExpressions/JoinExpression.cs
Implements IOptimizableEnumerableExpression TResult, adds optimization properties and AsOptimized/Merge methods for join projection merging.
GroupJoin Expression Optimization
Expressions/Expressions.Linq/EnumerableExpressions/GroupJoinExpression.cs
Implements IOptimizableEnumerableExpression TResult, adds optimization properties and AsOptimized/Merge methods for group join projection merging.
OrderBy/ThenBy Optimization Support
Expressions/Expressions.Linq/EnumerableExpressions/OrderByExpression.cs, ThenByExpression.cs
Adds ISQO interface and OptSource property to enable orderby expressions to participate in optimization chains as sources.
Test Coverage for Optimization
Expressions/Tests/Models.Expressions.Tests/NotifySystemTests.cs
Adds test methods NotifySystem_Optimize_SelectSelect, NotifySystem_Optimize_SelectSelectSelect, NotifySystem_Optimize_SelectManySelect to verify query optimization produces correct incremental results.

Sequence Diagram

sequenceDiagram
    participant Client
    participant SelectExpression
    participant QueryOptimizer
    participant ProjectionMergeQueryOptimizer
    participant ExpressionVisitor
    Client->>SelectExpression: AsNotifiable()
    SelectExpression->>QueryOptimizer: Optimize<TResult>(this)
    QueryOptimizer->>QueryOptimizer: Check if Source is IOptimizableEnumerableExpression
    alt Source is optimizable
        QueryOptimizer->>SelectExpression: Set PrevExpression = OptSelectorExpression
        SelectExpression-->>QueryOptimizer: Return modified expression
        QueryOptimizer->>QueryOptimizer: Recursively optimize
    end
    QueryOptimizer->>ProjectionMergeQueryOptimizer: MergeLambdaExpressions(prevSelector, currentSelector)
    ProjectionMergeQueryOptimizer->>ExpressionVisitor: Visit(firstLambda)
    ExpressionVisitor-->>ProjectionMergeQueryOptimizer: Merged lambda
    ProjectionMergeQueryOptimizer-->>SelectExpression: New merged SelectExpression
    SelectExpression-->>Client: INotifyEnumerable with optimized query
Loading

Dig Deeper With Commands

  • /review <file-path> <function-optional>
  • /chat <file-path> "<question>"
  • /roast <file-path>

Runs only when explicitly triggered.

@georghinkel georghinkel marked this pull request as ready for review January 16, 2026 14:20
@georghinkel
Copy link
Copy Markdown
Contributor Author

/roast Expressions/Expressions.history

@mergemonkeyhq
Copy link
Copy Markdown

mergemonkeyhq bot commented Jan 16, 2026

/roast Expressions/Expressions.history

  • "Wow, a whole line added to the history file—groundbreaking! Did you optimize the query or just the effort to write this commit message? Either way, I'm sure the performance gains are historic."

@georghinkel
Copy link
Copy Markdown
Contributor Author

/roast Expressions/Expressions.Linq/ExpressionsOptimization/ProjectionMergeQueryOptimizer.cs

@mergemonkeyhq
Copy link
Copy Markdown

mergemonkeyhq bot commented Jan 16, 2026

/roast Expressions/Expressions.Linq/ExpressionsOptimization/ProjectionMergeQueryOptimizer.cs

  • "This code is like a Swiss Army knife—full of tools, but you're not sure which one will actually cut it. From the 15 ObservingFunc types (because why stop at 10?) to the dynamic bomb in CreateObservingFunc, it’s clear someone was really committed to making future maintainers question their life choices. And let’s not forget the OptimizationVaribable typo—because nothing says 'optimized' like a misspelled variable. Also, throwing ArgumentNullException with raw strings? Bold move, assuming no one will ever debug this. 10/10 for creativity, 0/10 for mercy."

@georghinkel
Copy link
Copy Markdown
Contributor Author

/chat Expressions/Expressions.Linq/ExpressionsOptimization/ProjectionMergeQueryOptimizer.cs "How does the optimization work?"

@mergemonkeyhq
Copy link
Copy Markdown

mergemonkeyhq bot commented Jan 16, 2026

/chat Expressions/Expressions.Linq/ExpressionsOptimization/ProjectionMergeQueryOptimizer.cs

Q: How does the optimization work?

The optimization works by merging two consecutive lambda expressions into one. The key points are:

  1. The Optimize method takes two expressions and merges them using MergeLambdaExpressions.
  2. MergeLambdaExpressions uses a QueryOptimizerVisitor to visit and transform the first expression, then combines it with the second expression's body.
  3. For expressions with variables, MergeLambdaExpressionsWithVariables introduces an optimization variable to represent the second expression.
  4. The optimizer handles specific expression types (SelectExpression, SelectManyExpression, JoinExpression, GroupJoinExpression) by merging their selector expressions.
  5. The OptimizeExpression methods apply these optimizations to the given expressions, ensuring that consecutive expressions are merged into a single optimized expression.

@georghinkel
Copy link
Copy Markdown
Contributor Author

/roast Expressions/Expressions.Linq/ExpressionsOptimization/QueryOptimizerVisitor.cs

@mergemonkeyhq
Copy link
Copy Markdown

mergemonkeyhq bot commented Jan 16, 2026

/roast Expressions/Expressions.Linq/ExpressionsOptimization/QueryOptimizerVisitor.cs

  • "Ah, the classic 'stringly-typed' parameter comparison! Because nothing says 'I trust my to-string implementation' like comparing parameter names with .Equals(node.ToString()). Also, love how you're optimizing queries by potentially swapping entire lambda bodies—because who needs type safety when you've got ambition? 🔥"

@georghinkel
Copy link
Copy Markdown
Contributor Author

/chat Expressions/Expressions.Linq/ExpressionsOptimization/QueryOptimizerVisitor.cs "How does this class modify expressions?"

@mergemonkeyhq
Copy link
Copy Markdown

mergemonkeyhq bot commented Jan 22, 2026

/chat Expressions/Expressions.Linq/ExpressionsOptimization/QueryOptimizerVisitor.cs

Q: How does this class modify expressions?

The QueryOptimizerVisitor class modifies expressions by replacing a specific parameter in the expression tree with either a provided OptimizationVariable or the body of a second lambda expression. Specifically, it checks if a parameter in the expression matches the parameter of the first lambda expression (_firstLambdaSelectorExpression). If it does, it replaces that parameter with the OptimizationVariable (if set) or the body of the second lambda expression (_secondLambdaSelectorExpression.Body). This is done in the overridden VisitParameter method.

public Expression MergeLambdaExpressions(LambdaExpression firstLambdaExpression, LambdaExpression secondLambdaExpression)
{
var queryOptimizerVisitor = new QueryOptimizerVisitor(firstLambdaExpression, secondLambdaExpression);
var builtExpression = queryOptimizerVisitor.Visit(firstLambdaExpression) as LambdaExpression;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merged selectors are built in the wrong direction

The projection merger walks the previous selector and substitutes its parameter with the later selector body, then reuses the later selector's parameter list. With concrete selectors like x => x.ToString() then s => s.Length, that builds the wrong expression shape instead of x => x.ToString().Length, so optimized Select chains can return the wrong projection or fail while constructing the replacement lambda.

Compose by visiting the later selector and replacing its parameter with the earlier selector body, so the merged lambda is current(previous(x)).


var param = _firstLambdaSelectorExpression.Parameters[0];

if (node.ToString().Equals(param.ToString()))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parameter matching by ToString() rewrites the wrong variables

The optimizer identifies parameters by their printed name instead of the actual ParameterExpression instance. That breaks nested lambdas that reuse the same parameter name, because inner-scope variables get substituted even though they are different bindings.

Match parameters by reference identity, and avoid descending into nested lambdas unless their parameters are intentionally part of the substitution.

if (expression.PrevExpression == null)
throw new ArgumentNullException("expression.PrevExpression");

var mergedSelectorExpression = this.Optimize<TSource, TResult, TOptimizedResult>(expression.PrevExpression,expression.ResultSelector) as Expression<Func< TSource, TIntermediate, TOptimizedResult >>;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multi-parameter selector optimization leaves unbound parameters in the merged lambda

SelectMany, Join, and GroupJoin all reuse the same one-parameter substitution routine, but their result selectors take two parameters. The merger only replaces parameter 0 and then rebuilds the lambda with the previous selector's parameter list, so the second parameter remains in the body without being declared.

Handle multi-parameter result selectors with a parameter map that preserves the untouched parameters from the later selector.

@mergemonkeyhq
Copy link
Copy Markdown

mergemonkeyhq bot commented Apr 6, 2026

Actionable Comments Posted: 3

👀 Worth a Look (1)
[MEDIUM] Optimization state is stored on expression instances and never cleared - Expressions/Expressions.Linq/EnumerableExpressions/SelectExpression.cs (36, 38)
The optimizer mutates \`PrevExpression\` on existing nodes while walking backward through the chain, and I couldn't find any cleanup. If the same expression object is optimized more than once, a stale previous selector may still be attached and influence a later optimization pass.

Prefer passing the previous selector as an argument during optimization, or clear \`PrevExpression\` after the merged node has been produced.
🧾 Coverage Summary
✔️ Covered (14 files)
- Expressions/Expressions.Linq/EnumerableExpressions/GroupJoinExpression.cs
- Expressions/Expressions.Linq/EnumerableExpressions/JoinExpression.cs
- Expressions/Expressions.Linq/EnumerableExpressions/OrderByExpression.cs
- Expressions/Expressions.Linq/EnumerableExpressions/SelectExpression.cs
- Expressions/Expressions.Linq/EnumerableExpressions/SelectManyExpression.cs
- Expressions/Expressions.Linq/EnumerableExpressions/ThenByExpression.cs
- Expressions/Expressions.Linq/ExpressionsOptimization/IQueryOptimizer.cs
- Expressions/Expressions.Linq/ExpressionsOptimization/OptimizableExpressionExtensions.cs
- Expressions/Expressions.Linq/ExpressionsOptimization/ProjectionMergeQueryOptimizer.cs
- Expressions/Expressions.Linq/ExpressionsOptimization/QueryOptimizer.cs
- Expressions/Expressions.Linq/ExpressionsOptimization/QueryOptimizerVisitor.cs
- Expressions/Expressions.Linq/Facade/IEnumerableExpression.cs
- Expressions/Expressions.history
- Expressions/Tests/Models.Expressions.Tests/NotifySystemTests.cs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants