-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
First class span's break EFC #109757
Comments
Tagging subscribers to this area: @cston |
cc @jjonescz |
@Suchiman Which version of VS are you on? 17.13 Preview 1? I've just come across this when building the tests on efcore (same problem with MemoryExtesions.* vs IEnumerable.*) To me it only happened on 17.13 preview 1 AND |
yes |
I've attached a simple repro application ConsoleApp2.zip On Visual Studio 17.12 it works, in 17.13p1 I get this issue with both .NET 8 and .NET 9 if LangVersion is set to preview |
Any relation to #96160 ? |
This was discussed at LDM with no change in the C# language planned. LINQ-to-DB should be probably updated to recognize |
Does this mean libraries need to fix this issue? In that case it would mean .NET 9 (or lower) can never use the new C# LangVersion together with EF Core when First class span's get released? I know using newer C# LangVersions in older .NET versions isn't official supported, but I do this together with PolySharp a lot. When I upgraded my .NET SDK locally, I got hit by this issue; since I had |
You can use .NET 9, just make sure you call |
This problem is not specific to EF Core. You can hit it with plain vanilla System.Linq.Expressions. It can be fixed in System.Linq.Expressions by adding support for byref-like types to the Interpreter, but that is in conflict with the effectively archived status of System.Linq.Expressions. @jjonescz This needs a breaking change notice to be filled. cc @jaredpar |
Added
Tagging @dotnet/compat for awareness of the breaking change. |
Just to second @jkotas's comment, our problem in EF is not recognizing MemoryExtensions.Contains and similar (though that in itself is also a breaking change) - it's the reliance on the LINQ interpreter in various scenarios, but the interpreter doesn't support this. More generally, there's a gap that's starting to widen between C# itself and its metaprogramming (and related) features: the interpreter can no longer handle any construct, and LINQ expression trees themselves only represent a small subset of C# (and getting smaller). This will become more and more problematic, unless a decision is made to start updating these components to bring them more in line with the latest C# developments. |
That doc says "We will consider changes that address significant bugs or regressions". This seems like a significant regression.
So... does EF need to implement its own interpreter to avoid relying on an archived component? |
Well, the LINQ interpreter in .NET is a general component used by other users and packages out there, other than EF - they are all going to be similarly broken by this change; so if someone (regardless of who does the work) adds support to the interpreter, I don't believe that should be part of the EF package. This is probably a conversation best taken offline... |
@jjonescz There's already 2 issues previously related to byref-like types and Span types in expression trees (#96160 and #27499). Both have basically said the same as @jkotas - that With the new first class span support being introduced, it's clear that there would be an increasing likelihood of more expression trees encountering the byref-like types. With this in mind, I feel the impact of that bug has changed enough to at the very least needing to be re-evaluated (and possibly implemented) |
A hacky workaround is to make an ExpressionVisitor that converts all the CodeThe ExpressionVisitor: using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
internal class SpanToEnumerableVisitor : ExpressionVisitor
{
private static readonly SpanToEnumerableVisitor Instance = new();
private static readonly MethodInfo ContainsSpan = typeof(MemoryExtensions).GetMethod(
"Contains",
BindingFlags.Public | BindingFlags.Static,
null,
[typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0)), Type.MakeGenericMethodParameter(0)],
null
)!;
private static readonly MethodInfo ContainsEnumerable = typeof(Enumerable).GetMethod(
"Contains",
BindingFlags.Public | BindingFlags.Static,
null,
[typeof(IEnumerable<>).MakeGenericType(Type.MakeGenericMethodParameter(0)), Type.MakeGenericMethodParameter(0)],
null
)!;
protected override Expression VisitMethodCall(MethodCallExpression node)
{
if (node.Method.IsGenericMethod &&
node.Method.GetGenericMethodDefinition() == ContainsSpan &&
node.Method.GetGenericArguments() is [var type] &&
node.Arguments[0] is MethodCallExpression { Method.Name: "op_Implicit" } callExpression &&
callExpression.Arguments[0].Type == type.MakeArrayType())
{
return Expression.Call(
ContainsEnumerable.MakeGenericMethod(type),
Expression.Convert(callExpression.Arguments[0], typeof(IEnumerable<>).MakeGenericType(type)),
node.Arguments[1]
);
}
return base.VisitMethodCall(node);
}
public static T Convert<T>(T expression) where T : Expression => (T)Instance.Visit(expression);
} Example: string[] filters = ["Test"];
Expression<Func<MyTable, bool>> filter = x => filters.Contains(x.Name);
// Comment out the next line to check the exception
filter = SpanToEnumerableVisitor.Convert(filter);
Func<MyTable, bool> func = filter.Compile(preferInterpretation: true);
bool result = func(new MyTable { Name = "Test" });
Console.WriteLine(result);
public class MyTable
{
public int Id { get; set; }
public string Name { get; set; }
} |
|
Yep, that's likely the workaround we'd do in EF for this. |
While |
@ChrisJollyAU indeed. |
@jjonescz @jkotas Was also thinking on this. EFC calls the With the function being as follows
Clearly the Even if the byref-like types wasn't added to the interpreter (if the verdict from the other mentioned issues is still valid now), I would say this part would be applicable - to make sure that the @roji This should satisfy you with regards to performance - those normal expressions will just be interpreted as normal, and then drop into the full compiler (which does work) for the restricted types |
Filed dotnet/docs#43952.
It seems this would also require adding support for ref structs in reflection since the interpreter is using reflection under the hood.
This sounds like a potentially good solution to me - avoid interpretation if the expression tree contains spans. But is it possible to do compilation everywhere or is it necessary to fall back to interpretation in some cases (e.g., on some platforms)? |
I think it's just performance reasons that EFC uses the interpreter. The expressions are evaluated only once so was faster with the interpreter than with the compilation and its overheads. @roji Any other reasons for using the interpreter? |
This does not work for native AOT that EF Core has been busy adding support for. |
Yes, adding support for ref structs in reflection has been on the reflection backlog for a while. So far we did not have a good motivating scenario to justify investing into it. |
@ChrisJollyAU and others, I think the EF strategy for dealing with this will be to perform early detection and replacing of expression nodes that cannot be interpreted, basically transforming the new tree back to the way it was - before the LINQ interpreter is ever invoked. In other words, we'd have a static, hard-coded table in EF's funcletizer (the very first visitor that processes the incoming the tree), and would simply rewrite the tree. So from a pure EF perspective, we'll likely be able to deal with this without any need to either disable interpretation, or something like CanInterpret. Do you see any trouble with that approach @ChrisJollyAU? Regardless, this will affect other users of the LINQ interpreter. I'm not sure CanInterpret would be very useful to them, and I'm generally not in love with "magically" switching between interpretation and compilation based on something that the user has no knowledge or control over (i.e. there's a performance cliff the moment you introduce a Contains, or possibly other discrepancies). I'd at least want to discuss this with other users of the interpreter first...
Yep, that was the reason, nothing else. |
Description
When enabling
LangVersion
preview
, that enables first class span's which then prefersMemoryExtensions.Contains
overEnumerable.Contains
which then breaks EFC. It seems that EFC internally callsExpression.Compile
withpreferInterpretation
set totrue
which then eventually crashes.Reproduction Steps
Expected behavior
First class span's should not negatively affect EFC queries
Actual behavior
Regression?
Yes, only when enabling
<LangVersion>preview</LangVersion>
Known Workarounds
set
<LangVersion>preview</LangVersion>
to anything but previewConfiguration
.NET 9.0.0
EFC 9.0.0
Other information
No response
The text was updated successfully, but these errors were encountered: