[Proposal]: Expression tree evolution #4727
-
Expression tree evolution
SummaryThis proposal provides a way to introduce changes to the expression trees generated by the compiler, while providing a reasonable backwards compatibility story for legacy LINQ providers and other consumers. MotivationThe expression trees haven't had significant updates since introduction and lack many C# features that have now become commonplace. This creates a perception of staleness for customers and makes them doubt the future of expression trees. This is a non-exhaustive list of C# features that are currently either unsupported or are more restrictive in compiler-generated expression trees:
See ExpressionFutures for more details and implementation prototype. Related discussions: #158, #2029, #2545 In addition to the above there are proposed LINQ features that would also benefit from the proposed pattern: Detailed designExpressionTreeLangVersion
If a language feature that isn’t supported by the configured version is used in the expression the compiler outputs an error message that presents the user with information on how to change the language version and advises them to check their LINQ provider’s or expression-processing library documentation for the latest supported expression tree version. As long as all the used features are supported changing The default implementation of Expression tree versioning pattern is independent of ExpressionBuilderAttributenamespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)]
public class ExpressionBuilderAttribute : Attribute
{
public ExpressionBuilderAttribute(Type type)
{
BuilderType = type;
}
public Type BuilderType { get; }
}
} When For example, the following builder limits the expression trees to just a subset of three expression node types: public static class LimitingExpressionBuilder
{
public static ConstantExpression Constant(object value, Type type) => Expression.Constant(value, type);
public static ParameterExpression Parameter(Type type, string name) => Expression.Parameter(type, name);
public static BinaryExpression Add(Expression left, Expression right) => Expression.Add(left, right);
}
Non-annotated methods are treated as annotated with
When an
All LINQ operator methods are annotated with When an expression tree is assigned to a variable before being passed to the method annotated with public class VersionedExpression<TBuilder, TExpression>
where TExpression : Expression
{
public VersionedExpression(TExpression expression)
{
Expression = expression;
}
public TExpression Expression { get; }
public static implicit operator TExpression(VersionedExpression<TBuilder, TExpression> expression)
=> expression.Expression;
} For example: VersionedExpression<LimitingExpressionBuilder, Expression<Func<int>>> expr = () => 1 + 2;
var _ = Evaluate(expr);
[ExpressionBuilder(typeof(LimitingExpressionBuilder))]
public abstract int Evaluate(Expression<Func<int>> expr); If When using an See Expression Types proposal for a description of how
DrawbacksDynamic expression handlingThe above doesn’t attempt to protect against expression trees which are constructed dynamically (e.g. via
Similarly, layers such as Automapper or OData - which produce or transform expression trees - are also responsible for ensuring correctness and LINQ provider compatibility. In practice, since different providers support different tree shapes, layers such as OData already need to be aware of the LINQ provider being targeted, and tailor their output trees accordingly. The user would likely need to opt-in to a particular version by configuring a setting or installing a NuGet package. Multiple LINQ providersWhen multiple LINQ providers are used in the same application, the lowest supported language version must be selected to take advantage of compile-time checking. This can be resolved by casting to Alternative expression tree representationsSince the expression trees produced need to remain backward compatible some expressions will need be represented by a new expression type, even if semantically they could be grouped with an existing expression type. This would make the code that handles the expression trees more verbose. And this prevents from adding better representations of existing features, e.g. interpolated strings. AlternativesLet LINQ providers expose the supported expression versionThe initial experience/discovery is not ideal – the user uses a modern construct and gets a compilation error; they need to figure out how to set up the expression version and which version is supported by their LINQ provider. And if a version is selected that isn’t supported by a LINQ provider the behavior is undefined (and could include data corruption). The providers could offer a way to use the latest supported expression version in a way that could be consumed by the user project. The provider NuGet package can include a .targets file that adds a value to However in the rare case when multiple LINQ providers are referenced and one of them hasn't been updated with Default to the latest expression version for LINQ methods.Another approach would be to default to the latest expression version supported by the compiler and introduce Any custom queryable operators defined outside BCL would not have the Pros:
Cons:
Runtime checks by the queryable operatorsTo reduce the performance penalty on legacy providers of the previous approach we could introduce a Pros:
Cons:
Mirror C# versioning
However adding "1" and "2" would mean unnecessarily restricting the available features and starting at "3" for the current feature-set would be confusing as the expression trees in C# 9.0 don't support all C# 3.0 features. Using an independent pattern provides more flexibility to the order the expression tree features would be introduced. Introduce a new expression node hierarchySwitching to a new representation that's aligned with the internal Roslyn tree will make implementing new C# features less costly and having a consistent representation will make the implementation of non-LINQ consumers easier. However LINQ providers would still need to have backward-compatibility, resulting in considerable maintenance penalty. And having two or more ways of representing an operation in the expression tree could lead to confusion on both the user and consumer sides. Use pre-processor directives
However this doesn't allow to use an expression builder that produces a different type of expression nodes. Unresolved questionsDesign meetings |
Beta Was this translation helpful? Give feedback.
Replies: 15 comments 70 replies
-
If you look at the current implementation of expression trees you can see that the syntax-tree is not just transformed into an equilant expression tree. Expression<Func<int, short, int>> f = (a, b) => a + b; the generated code is: ParameterExpression parameterExpression = Expression.Parameter(typeof(int), "a");
ParameterExpression parameterExpression2 = Expression.Parameter(typeof(short), "b");
BinaryExpression body = Expression.Add(parameterExpression, Expression.Convert(parameterExpression2, typeof(int)));
ParameterExpression[] array = new ParameterExpression[2];
array[0] = parameterExpression;
array[1] = parameterExpression2;
Expression.Lambda<Func<int, short, int>>(body, array) instead of just: ParameterExpression parameterExpression = Expression.Parameter(typeof(int), "a");
ParameterExpression parameterExpression2 = Expression.Parameter(typeof(short), "b");
BinaryExpression body = Expression.Add(parameterExpression, parameterExpression2);
ParameterExpression[] array = new ParameterExpression[2];
array[0] = parameterExpression;
array[1] = parameterExpression2;
Expression.Lambda<Func<int, int, int>>(body, array); Applying all these tiny little things makes it hard to maintain expression tree generation in the compiler. If we want to extend the expression-tree support I would like to ignore these things and create just a raw-transformation of the syntax-tree into an expression tree. Another approach would be something like "roslynQuoter" but for expression trees. |
Beta Was this translation helpful? Give feedback.
-
off-topic: icmyi https://github.com/reaqtive/reaqtor was open-sourced by @bartdesmet moments ago. If you like expression trees then there are 15 years of implemented knowledge right there. |
Beta Was this translation helpful? Give feedback.
-
Question: instead of being a compiler option (and therefore effectively a dialect option) couldn't this be specified purely by the SDK, in the same manner as AssemblyVersionAttribute is specified now? That way, Roslyn and the language spec would only specify what the translation is (i.e. |
Beta Was this translation helpful? Give feedback.
-
One thing, not directly related to the language itself, I would like to see is caching the expression trees when possible, like is done already for lambdas: Func<object, string> f = o => o.ToString(); // the Func<object, string> is reused for every subsequent call
Expression<Func<object, string>> e = o => o.ToString(); // the Expression instance is created every time Reusing the Expression instance (when possible) would make it better for things like conversion to SQL as the converted query could be cached based on the identity of the Expression instance. I am not sure how this might be a breaking change (expression trees are already immutable) but, if there would be an issue with that, I had the idea for the following syntax: Expression<Func<object, string>> e = static o => o.ToString(); There's a possibility to reuse this |
Beta Was this translation helpful? Give feedback.
-
I am very interested to stay updated as much as possible to any smallest implementation/change/evolution. |
Beta Was this translation helpful? Give feedback.
-
As far as I can see this proposal isn't currently Championed which means that it's not being worked on by the team at this time. I'd love to see something along these lines too, though. One specific detail about the proposal that I'd like to address: it seems to still be definitively tied to What if the [ExpressionBuilder(typeof(MyExpressionBuilder))]
public class MyExpression {
// whatever implementation here
}
public static class MyExpressionBuilder {
public static MyExpression Parameter(Type type, string name) => new MyExpression(something);
public static MyExpression Add(MyExpression left, MyExpression right) => new MyExpression(something);
}
MyExpression expr = (a, b) => a + b; By itself this isn't a fully formed proposal - for example, it doesn't explain how the types of In general, the idea would be that the BCL could annotate This also, to some extent, resolves the language evolution problem, in that a whole new type could be defined for the newer version, and if a newer language construct were used with the old |
Beta Was this translation helpful? Give feedback.
-
@JeremyLikness are you still championing this from an EF perspective? |
Beta Was this translation helpful? Give feedback.
-
Perhaps a better way to enable quoting of code (which is the problem expression trees solve) would be an approach like Scala macros (https://docs.scala-lang.org/overviews/macros/overview.html). Pretty much, you'd do
where MyImplementation would depend on the compiler AST, but we already have source generators that work like that. |
Beta Was this translation helpful? Give feedback.
-
No updates on this? |
Beta Was this translation helpful? Give feedback.
-
+1 For this proposal. For instance, I'm tired of: What I'm trying to generate: |
Beta Was this translation helpful? Give feedback.
-
My understanding is that this work has so far not met the bar for investment. That could change, but I don't expect it to any time soon. I think we are back to the status quo from the last few years that System.Expressions is what it is. |
Beta Was this translation helpful? Give feedback.
-
Could we not start with the easy ones to get the ball moving. Most of the main pain points could be lowered to the current expression tree nodes: Conditional access expressionsTranslate Named and optional parametersGiven
Translate Tuple literalsTranslate Tuple equality and inequality
Translate This 4 things, but specially the first one, will cover most of the paint points with minimal braking changes, if any. Some other expressions like Dictionary initializes, Array initializers, Discard parameters, etc could also be implemented as a lowering-only solution. Things like Pattern Matching / switch expressions, while useful, will have limited use in practice because the Where can not give parameters to the Select. Also are very hard to implement from the Roslyn side and from the LINQ provider side. As for the statement/assignments nodes, they where discarded in C# 3 because the focus was on SQL-translatable expressions, and this hasn't changed much. Async/await inside queries looks like a corner case for me and dynamic could be useful in theory but then... just use SQL instead of LINQ. As a LINQ provider implementation, I would like to give expressive power to the consumer user, but also have a reduced set of nodes to translate. Trying to solve the general solution is stopping all progress here. |
Beta Was this translation helpful? Give feedback.
-
@olmobrutall The point is that the blockers here aren't technical. The blocker is that the .NET Directors have decided that we're not investing in this area. Nothing is going to happen unless that changes. |
Beta Was this translation helpful? Give feedback.
-
Currently I use do not use Entity Framework or expression trees with Linq. If we could allow data access without a fixed model aka dynamic database access the importance of expression trees might change... |
Beta Was this translation helpful? Give feedback.
-
Static abstract interface members should probably be added to the list.
|
Beta Was this translation helpful? Give feedback.
My understanding is that this work has so far not met the bar for investment. That could change, but I don't expect it to any time soon. I think we are back to the status quo from the last few years that System.Expressions is what it is.