Skip to content
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 support causes translation failure with Contains #35100

Closed
jamesgurung opened this issue Nov 13, 2024 · 44 comments
Closed

First-class span support causes translation failure with Contains #35100

jamesgurung opened this issue Nov 13, 2024 · 44 comments

Comments

@jamesgurung
Copy link

Since installing .NET 9, I have come across a bug in production where setting <LangVersion>preview</LangVersion> results in certain LINQ queries causing an InvalidOperationException. This did not happen with .NET 9 RC2, however now that I've installed the .NET 9.0.100 SDK it always occurs even if I roll back EF Core.

Minimal repro:

Project.csproj

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <LangVersion>preview</LangVersion> <!-- NO EXCEPTION IF THIS LINE IS REMOVED -->
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
  </ItemGroup>
</Project>

Program.cs

using Microsoft.EntityFrameworkCore;

var ctx = new BloggingContext();
ctx.Database.EnsureCreated();
var list = Array.Empty<int>();
var query = ctx.Blogs.Where(b => list.Contains(b.Id)).ToQueryString(); // <-- THROWS InvalidOperationException

public class BloggingContext : DbContext
{
  public DbSet<Blog> Blogs { get; set; }
  protected override void OnConfiguring(DbContextOptionsBuilder options)
  {
    options.UseSqlite("DataSource=file::memory:?cache=shared");
    options.LogTo(Console.WriteLine);
    options.EnableSensitiveDataLogging();
  }
}

public class Blog
{
  public int Id { get; set; }
}

Exception

System.InvalidOperationException: 'An exception was thrown while attempting to evaluate the LINQ query parameter expression 'op_Implicit(value(Program+<>c__DisplayClass0_0).list)'. See the inner exception for more information.'

   at Microsoft.EntityFrameworkCore.Query.Internal.ExpressionTreeFuncletizer.<Evaluate>g__EvaluateCore|70_0(Expression expression, String& parameterName, Boolean& isContextAccessor)
   at Microsoft.EntityFrameworkCore.Query.Internal.ExpressionTreeFuncletizer.Evaluate(Expression expression, String& parameterName, Boolean& isContextAccessor)
   at Microsoft.EntityFrameworkCore.Query.Internal.ExpressionTreeFuncletizer.ProcessEvaluatableRoot(Expression evaluatableRoot, State& state, Boolean forceEvaluation)
   at Microsoft.EntityFrameworkCore.Query.Internal.ExpressionTreeFuncletizer.EvaluateList(IReadOnlyList`1 expressions, State[] expressionStates, List`1& children, Func`2 pathFromParentGenerator)
   at Microsoft.EntityFrameworkCore.Query.Internal.ExpressionTreeFuncletizer.VisitMethodCall(MethodCallExpression methodCall)
   at Microsoft.EntityFrameworkCore.Query.Internal.ExpressionTreeFuncletizer.Visit(Expression expression, State& state)
   at Microsoft.EntityFrameworkCore.Query.Internal.ExpressionTreeFuncletizer.VisitLambda[T](Expression`1 lambda)
   at Microsoft.EntityFrameworkCore.Query.Internal.ExpressionTreeFuncletizer.Visit(Expression expression, State& state)
   at Microsoft.EntityFrameworkCore.Query.Internal.ExpressionTreeFuncletizer.VisitUnary(UnaryExpression unary)
   at Microsoft.EntityFrameworkCore.Query.Internal.ExpressionTreeFuncletizer.Visit(Expression expression)
   at Microsoft.EntityFrameworkCore.Query.Internal.ExpressionTreeFuncletizer.Visit[T](ReadOnlyCollection`1 expressions, Func`2 elementVisitor, StateType& aggregateStateType, State[]& expressionStates, Boolean poolExpressionStates)
   at Microsoft.EntityFrameworkCore.Query.Internal.ExpressionTreeFuncletizer.Visit(ReadOnlyCollection`1 expressions, StateType& aggregateStateType, State[]& expressionStates, Boolean poolExpressionStates)
   at Microsoft.EntityFrameworkCore.Query.Internal.ExpressionTreeFuncletizer.VisitMethodCall(MethodCallExpression methodCall)
   at Microsoft.EntityFrameworkCore.Query.Internal.ExpressionTreeFuncletizer.Visit(Expression expression, State& state)
   at Microsoft.EntityFrameworkCore.Query.Internal.ExpressionTreeFuncletizer.ExtractParameters(Expression expression, IParameterValues parameterValues, Boolean parameterize, Boolean clearParameterizedValues, Boolean precompiledQuery, IReadOnlySet`1& nonNullableReferenceTypeParameters)
   at Microsoft.EntityFrameworkCore.Query.Internal.ExpressionTreeFuncletizer.ExtractParameters(Expression expression, IParameterValues parameterValues, Boolean parameterize, Boolean clearParameterizedValues)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExtractParameters(Expression query, IParameterValues parameterValues, IDiagnosticsLogger`1 logger, Boolean compiledQuery, Boolean generateContextAccessors)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteCore[TResult](Expression query, Boolean async, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression)
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToQueryString(IQueryable source)
   at Program.<Main>$(String[] args) in D:\Projects\EFCoreDemo\Program.cs:line 6

Inner Exception

GenericArguments[1], 'System.Span`1[System.Int32]', on 'System.Linq.Expressions.Interpreter.FuncCallInstruction`2[T0,TRet]' violates the constraint of type 'TRet'.

   at System.RuntimeType.ValidateGenericArguments(MemberInfo definition, RuntimeType[] genericArguments, Exception e)
   at System.RuntimeType.MakeGenericType(Type[] instantiation)
   at System.Linq.Expressions.Interpreter.CallInstruction.GetHelperType(MethodInfo info, Type[] arrTypes)
   at System.Linq.Expressions.Interpreter.CallInstruction.SlowCreate(MethodInfo info, ParameterInfo[] pis)
   at System.Linq.Expressions.Interpreter.CallInstruction.Create(MethodInfo info, ParameterInfo[] parameters)
   at System.Linq.Expressions.Interpreter.LightCompiler.CompileMethodCallExpression(Expression object, MethodInfo method, IArgumentProvider arguments)
   at System.Linq.Expressions.Interpreter.LightCompiler.Compile(Expression expr)
   at System.Linq.Expressions.Interpreter.LightCompiler.CompileConvertUnaryExpression(Expression expr)
   at System.Linq.Expressions.Interpreter.LightCompiler.Compile(Expression expr)
   at System.Linq.Expressions.Interpreter.LightCompiler.CompileTop(LambdaExpression node)
   at System.Linq.Expressions.Expression`1.Compile(Boolean preferInterpretation)
   at Microsoft.EntityFrameworkCore.Query.Internal.ExpressionTreeFuncletizer.<Evaluate>g__EvaluateCore|70_0(Expression expression, String& parameterName, Boolean& isContextAccessor)

Version

EF Core version: 9.0.0
Database provider: Microsoft.EntityFrameworkCore.Sqlite
Target framework: .NET 9.0
Operating system: Windows 11
IDE: Visual Studio 2022 17.13 Preview 1

@roji
Copy link
Member

roji commented Nov 13, 2024

@jamesgurung I pasted both your csproj and Program.cs exactly as is with .NET SDK 9.0.100, and the program ran fine... Printing the result of ToQueryString() gives the following:

SELECT "b"."Id"
FROM "Blogs" AS "b"
WHERE "b"."Id" IN (
    SELECT "l"."value"
    FROM json_each(@__list_0) AS "l"
)

Can you please double-check and confirm?

@jamesgurung
Copy link
Author

Hi @roji, thanks for getting back so quickly! I'm consistently getting the error in Visual Studio 2022 17.13.0 Preview 1.0, but I've just checked and it runs fine on the command line with dotnet run. Does this mean it's a VS issue?

Image

@roji
Copy link
Member

roji commented Nov 13, 2024

Hmm, am not sure about that - it wouldn't make sense for VS to be the cause of a bug like that... @ajcvickers feel like trying to repro this?

@pinkfloydx33
Copy link

Is this related to this: dotnet/runtime#109757?

@roji
Copy link
Member

roji commented Nov 13, 2024

@pinkfloydx33 definitely seems related - let's see what happens there. If the change is intended on the .NET side, we could look at reacting to it here in EF.

@ChrisJollyAU
Copy link
Contributor

ChrisJollyAU commented Nov 14, 2024

I was trying to build main and later release/9.0 using VS 2022 17.13 preview 1

8>D:\toolkits\efcore\test\EFCore.Specification.Tests\CustomConvertersTestBase.cs(1187,49,1187,50): error CS0023: Operator '.' cannot be applied to operand of type 'void'
8>D:\toolkits\efcore\test\EFCore.Specification.Tests\CustomConvertersTestBase.cs(1188,49,1188,50): error CS0023: Operator '.' cannot be applied to operand of type 'void'
8>D:\toolkits\efcore\test\EFCore.Specification.Tests\Query\PrimitiveCollectionsQueryTestBase.cs(430,67,430,74): error CS8620: Argument of type 'string[]' cannot be used for parameter 'span' of type 'Span<string?>' in 'bool MemoryExtensions.Contains<string?>(Span<string?> span, string? value)' due to differences in the nullability of reference types.
8>D:\toolkits\efcore\test\EFCore.Specification.Tests\Query\PrimitiveCollectionsQueryTestBase.cs(433,68,433,75): error CS8620: Argument of type 'string[]' cannot be used for parameter 'span' of type 'Span<string?>' in 'bool MemoryExtensions.Contains<string?>(Span<string?> span, string? value)' due to differences in the nullability of reference types.
8>D:\toolkits\efcore\test\EFCore.Specification.Tests\Query\PrimitiveCollectionsQueryTestBase.cs(577,67,577,76): error CS8620: Argument of type 'string[]' cannot be used for parameter 'span' of type 'Span<string?>' in 'bool MemoryExtensions.Contains<string?>(Span<string?> span, string? value)' due to differences in the nullability of reference types.

On the CustomConvertersTestBase it is an error in this portion. Specifically, with the v.Reverse(). Instead of picking up the IEnumerable version it is picking up with MemoryExtensions.Reverse(Span). Defined as public static void Reverse<T> (this Span<T> span);

I will say that if I hover of the Reverse, the tooltip shows me the Span version, however if I click on it and navigate to its definition I come to the IEnumerable

b.Property(e => e.ByteArray5)
    .HasConversion(
        new ValueConverter<byte[], byte[]>(
            v => v.Reverse().Concat(new byte[] { 4, 20 }).ToArray(),
            v => v.Reverse().Skip(2).ToArray()),
        bytesComparer)
    .HasMaxLength(7);

Note that the Directory.Build.props for the test folder does define <LangVersion>preview</LangVersion>

Also, whether or not it has any part of this, the VS Installer for 17.13 preview 1 still installs 9.0.0-rc.2.24474.4 although it doesnt appear to be being used.

Somehow or other, at least in some instances when the LangVersion is on preview, it almost like it the preferences have changed so that it is preferring/finding the MemoryExtensions functions first before the Linq version

Edit to add: Tried in 17.12. and it looks fine there even when LangVersion is on preview. Appears to be something only popping up when you use 17.13 preview 1 AND LangVersion is on preview

@roji
Copy link
Member

roji commented Nov 14, 2024

Thanks for diving into it @ChrisJollyAU... That's all sounds quite odd indeed. It feels like we should wait and see if the change (and all its consequences) are indeed intentional and desired (in dotnet/runtime#109757); we can react in EF e.g. by normalizing MemoryExtensions.Reverse() to Enumerable.Reverse() at some point in the query pipeline (though there seem to be deeper consequences around generics, as that issue shows). But it seems prudent to first see what's said on the runtime side I think...

@ChrisJollyAU
Copy link
Contributor

@jamesgurung Can you try with VS 17.12? Even with LangVersion on preview? Would be nice for extra confirmation it only affects the preview branch of Visual Studio.

@roji I primarily use the preview branch of VS and haven't encountered this anywhere during the various 17.12 previews. Might just need to make a note of this as a known issue for 17.13 for now.

@ilGianfri
Copy link

I've attached a simple console app to repro this issue ConsoleApp2.zip

@ChrisJollyAU
Copy link
Contributor

@ilGianfri Tried in both VS 17.12 and VS 17.13p1 and had no errors

@ilGianfri
Copy link

@ilGianfri Tried in both VS 17.12 and VS 17.13p1 and had no errors

That's strange, I get this

Image

@ChrisJollyAU
Copy link
Contributor

@ilGianfri Yeah, I get that if I update the project to target .Net 9 (your attached is targeting net 8).

I only get that when using 17.13p1. If you also get it on 17.12 please try a full clean and rebuild so that there are no artifacts from the roslyn version used by 17.13p1

This is something you would only pick up from 17.13p1 (further details coming up)

@ilGianfri
Copy link

@ilGianfri Yeah, I get that if I update the project to target .Net 9 (your attached is targeting net 8).

I only get that when using 17.13p1. If you also get it on 17.12 please try a full clean and rebuild so that there are no artifacts from the roslyn version used by 17.13p1

This is something you would only pick up from 17.13p1 (further details coming up)

I tried to rebuild it using 17.12 and I don't get the crash, I get the crash if I rebuild it using using 17.13p1 on both .NET 8 and .NET 9

@ChrisJollyAU
Copy link
Contributor

@roji Been doing more research on this. It isn't a runtime issue but part of something with the roslyn compiler.

This is related to First class span types. For roslyn it was introduced in 17.13p1 (see https://github.com/dotnet/roslyn/blob/main/docs/Language%20Feature%20Status.md). There is already a breaking changes documentation on this already https://github.com/dotnet/roslyn/blob/main/docs/compilers/CSharp/Compiler%20Breaking%20Changes%20-%20DotNet%2010.md . Note that some things for Enumerable (like the Reverse) have been updated in net10.0 to be compatible so when efcore updates its TFM to net10.0 it should be fine by that stage. But keep an eye on it. There might be some further changes need to accept the first class spans.

From the c# language proposal https://github.com/dotnet/csharplang/blob/main/proposals/first-class-span-types.md it is planned for C# 14. By the looks of it on VS 17.12 langversion preview has no effect since that feature isnt part of roslyn. In 17.13p1 it is there but currently only as a preview feature. Hence it only picks it up on preview.

For the current InvalidOperationException, I believe it is ultimately a compiler mismatch causing it. Reason being on 17.13p1 you are using a version of roslyn and compiling it with an upcoming feature that produces code that uses the Span types before any Enumerable (so you have method signatures related to that).

So directly compiled the below line with preview on would use Span<string>.Contains with 17.13p1 and IEnumerable<string>.Contains with preview not there or on 17.12

var a = nomiTipiAzione.Contains("Test");

This is fine on directly compiled code, however efcore uses some dynamic compilation. When running you are running on .Net 9 which would use a version of roslyn that doesn't know of first class spans (aka equivalent to VS 17.12). Thus when it compiles while running the method signature it creates would be on the IEnumerable on not Span and hence the failure

roji added a commit to roji/efcore that referenced this issue Nov 15, 2024
@roji
Copy link
Member

roji commented Nov 15, 2024

Thanks for this thorough explanation! I'm not exactly following everything, but based on your recommendation will place this issue in 10 for follow-up later... I'm also proposing we switch the project's <LangVersion> to preview, so that we catch this kind of thing in our CI etc. (#35121).

As far as you can understand, is there potential a change needed in the LINQ interpreter to react to these changes?

@roji roji added this to the 10.0.0 milestone Nov 15, 2024
@ChrisJollyAU
Copy link
Contributor

ChrisJollyAU commented Nov 15, 2024

@roji As far as I can tell, for .Net/EF Core 9 GA, there is nothing needed to be done

Once efcore starts switching to the net10.0 tfm there could be some issues. It's still early in that development so hard to say. If its just the stuff from IEnumerable then might not be an issue as .Net 10 would be updated so that things like Contains,Reverse etc still end up on the IEnumerable version and not the Span version

Quick summary:

  1. This problem only occurs if using VS 17.13 preview 1 (and later) AND you have LangVersion set to preview
  2. The roslyn compiler introduced support for first class span types in VS 17.13 p1 as part of c# 14
  3. LangVersion of preview enables the new c# 14 features
  4. With this, Span<T> overloads are preferred. If there is no Span<T> overload on e.g. an IEnumerable method (Contains), it will fallback on the direct Span<T> type from MemoryExtensions potentially causing problems
  5. InvalidOperationException can occur because the original roslyn that built the program (from VS 17.13p1) output a binary that directly used the first class span types in places where it was previously an IEnumerable. However, EF Core does some compilation in runtime which as its running in .Net 9, is effectively using a different version of the roslyn compiler (aka one that doesn't know about the new feature). This produces a slightly different method to what the binary knows about. Hence the exception

@roji
Copy link
Member

roji commented Nov 15, 2024

Thanks. Just about this:

However, EF Core does some compilation in runtime which as its running in .Net 9, is effectively using a different version of the roslyn compiler (aka one that doesn't know about the new feature). This produces a slightly different method to what the binary knows about. Hence the exception

Looking at the original error message and stack traces, this isn't about compilation in runtime: it's EF running the LINQ interpreter, which is part of the .NET runtime (nothing to do with Roslyn or compilation). In other words, it's maybe the runtime that needs to react to the new C# 14 feature, by making sure that the LINQ interpreter works correctly with the LINQ trees that the compiler now produces (different than the previous ones).

In other words, if my understanding here is correct, then it should be possible to create a minimal console program which uses the LINQ interpreter directly (like EF does), showing a simple LINQ construct that used to work and now fails with the VS etc.

@ChrisJollyAU
Copy link
Contributor

ChrisJollyAU commented Nov 15, 2024

@roji Initially it does look like part of Linq but if you go deeper into EF Core you end up in the ExpressionTreeFuncletizer

Image

Specifically this is the code

try
{
    return Lambda<Func<object>>(
            Convert(expression, typeof(object)))
        .Compile(preferInterpretation: true)
        .Invoke();
}
catch (Exception exception)
{
    throw new InvalidOperationException(
        _logger.ShouldLogSensitiveData()
            ? CoreStrings.ExpressionParameterizationExceptionSensitive(expression)
            : CoreStrings.ExpressionParameterizationException,
        exception);
}

Note the .Compile, why is why I'm trending towards a compilation issue with a different version of roslyn

Edit to add: The test app that @ilGianfri did, is a nice simple one to start playing around on

@roji
Copy link
Member

roji commented Nov 15, 2024

Note the .Compile, why is why I'm trending towards a compilation issue with a different version of roslyn

Yeah, the naming is confusing, but note preferInterpretation: true. "Compile" in this context means "produce a runnable lambda I can invoke"; by default this indeed compiles the expression tree, and the lambda runs like regular code (with the JIT), but with preferInterpretation: true an interpreter is used instead. See also the System.Linq.Expressions.Interpreter references in the stack trace reported above:

   at System.RuntimeType.ValidateGenericArguments(MemberInfo definition, RuntimeType[] genericArguments, Exception e)
   at System.RuntimeType.MakeGenericType(Type[] instantiation)
   at System.Linq.Expressions.Interpreter.CallInstruction.GetHelperType(MethodInfo info, Type[] arrTypes)
   at System.Linq.Expressions.Interpreter.CallInstruction.SlowCreate(MethodInfo info, ParameterInfo[] pis)
   at System.Linq.Expressions.Interpreter.CallInstruction.Create(MethodInfo info, ParameterInfo[] parameters)
   at System.Linq.Expressions.Interpreter.LightCompiler.CompileMethodCallExpression(Expression object, MethodInfo method, IArgumentProvider arguments)
   at System.Linq.Expressions.Interpreter.LightCompiler.Compile(Expression expr)
   at System.Linq.Expressions.Interpreter.LightCompiler.CompileConvertUnaryExpression(Expression expr)
   at System.Linq.Expressions.Interpreter.LightCompiler.Compile(Expression expr)
   at System.Linq.Expressions.Interpreter.LightCompiler.CompileTop(LambdaExpression node)
   at System.Linq.Expressions.Expression`1.Compile(Boolean preferInterpretation)
   at Microsoft.EntityFrameworkCore.Query.Internal.ExpressionTreeFuncletizer.<Evaluate>g__EvaluateCore|70_0(Expression expression, String& parameterName, Boolean& isContextAccessor)

We prefer to interpret here rather than compile, since in this particular case we know that the resulting lambda will only ever be invoked once (that's the Invoke() just afterwards). The overhead of actually compiling is far too great for this one-off invocation (see #29814).

@ChrisJollyAU
Copy link
Contributor

@roji Yeah I had started thinking the compilation part was starting to go down the wrong path.

Busy debugging even more (even better now that I sorted my compilation errors out).

  1. Using first class span types introduces a new node to the expression tree. It is a method call. Name of op_Implicit. Takes 1 parameter (the normal array e.g. T[]) and returns Span<T>
  2. The parameter in the function it is being used in gets its signature changed (so with Contains the parameter is e.g. Span`)
  3. This seems to mostly affect Contains for now.
  4. Given the signature of Contains has changed it doesnt find any matches in the various Translate functions.
  5. For the above lambda code, we end up having expression having a type of Span<T> (of which I believe Span is a struct) and then it is adding a convert to object. Does that even make sense converting a struct to object?

TO DO

  1. Handle the new implicit convert methodcall expressions
  2. Match against the Span types as well for the translators in addition to the IEnumerable
  3. Not sure what to do in the funcletizer? Perhaps just return expression without any additional converts?

@pinkfloydx33
Copy link

Does that even make sense converting a struct to object?

Span<> is a ref struct and cannot be boxed to object. It also can't be a type parameter, unless the method has the allows ref struct anti-constraint.

Can Expressions even handle these cases generally?

@roji
Copy link
Member

roji commented Nov 16, 2024

Yeah, I'm just noting here that the OP's stack trace shows the following error:

GenericArguments[1], 'System.Span`1[System.Int32]', on 'System.Linq.Expressions.Interpreter.FuncCallInstruction`2[T0,TRet]' violates the constraint of type 'TRet'.
   at System.RuntimeType.ValidateGenericArguments(MemberInfo definition, RuntimeType[] genericArguments, Exception e)
   at System.RuntimeType.MakeGenericType(Type[] instantiation)
   at System.Linq.Expressions.Interpreter.CallInstruction.GetHelperType(MethodInfo info, Type[] arrTypes)
   at System.Linq.Expressions.Interpreter.CallInstruction.SlowCreate(MethodInfo info, ParameterInfo[] pis)
   at System.Linq.Expressions.Interpreter.CallInstruction.Create(MethodInfo info, ParameterInfo[] parameters)
   at System.Linq.Expressions.Interpreter.LightCompiler.CompileMethodCallExpression(Expression object, MethodInfo method, IArgumentProvider arguments)
   at System.Linq.Expressions.Interpreter.LightCompiler.Compile(Expression expr)
   at System.Linq.Expressions.Interpreter.LightCompiler.CompileConvertUnaryExpression(Expression expr)
   at System.Linq.Expressions.Interpreter.LightCompiler.Compile(Expression expr)
   at System.Linq.Expressions.Interpreter.LightCompiler.CompileTop(LambdaExpression node)
   at System.Linq.Expressions.Expression`1.Compile(Boolean preferInterpretation)
   at Microsoft.EntityFrameworkCore.Query.Internal.ExpressionTreeFuncletizer.<Evaluate>g__EvaluateCore|70_0(Expression expression, String& parameterName, Boolean& isContextAccessor)

This is clearly the interpreter having trouble with the LINQ expression tree that's now getting generated by compiler. I'd repro this outside of EF (just a console program doing .Compile(preferInterpretation: true), and submit that as the error for the runtime folks to ponder over.

@ChrisJollyAU
Copy link
Contributor

@roji There is also the fact that we are doing that Lambda call (with the added Convert) at a part where we probably shouldn't do it.

In the example above, when we get to EvaluateCore in C# 13, the expression is a MemberAccess node which is handled easily. The lambda function does not get called

In C# 14, it gets wrapped in a op_Implicit method call, which doesn't hit the Member Access handling and hits the fall back for the lambda. The return type of this expression is a Span<T> type which as mentioned above I have my doubts if it can be wrapped in a convert to object call (object vs struct). We could just be falling back on to that and trying to do something unsupported. Thats my current working theory on that error (especially because its complaining about TRet)

On a different note, have made a lot of progress in updating ef core to handle/behave nicely with first class spans.

I do have a noticeable change to ask about. I've found that I end up inlining a lot more arrays into the sql query and not using it as a parameter.

This only occurs on plain arrays (so something like string[]). Up until now in the QueryRootProcessor it has created a ParameterQueryRootExpression for that parameter. However that has a Type of IQueryable. If I update the expression to add that in, it fails as the expression it is being used in is expecting a Span type so the validation fails.

Without using the ParameterQueryRootExpression ultimately the array gets inlined as part of the raw sql.

Thoughts on leaving it like that or making ParameterQueryRootExpression Span compatible ?
PS. For SqlServerFunctionalTests I currently have 197 failing and of that 175 are the above scenario.

@davidroth
Copy link
Contributor

davidroth commented Dec 19, 2024

@Suchiman @ChrisJollyAU

Yep you are right. Calling AsEnumerable() is indeed accepted by EF6 and it works.

Unfortunately, there is no easy way to find all occurrences that need to be updated, unless each query is covered by an integration test. Otherwise I have to wait for runtime crashes and fix them on a case-by-case basis, or I have to find all occurrencies where I have a linq expression with such a condition (array + contains) in advance. But AFAIK there is not an easy way to reliably find and fix all occurencies automatically. E.g. when searching for .Contains( in the Assembly containing our queries, I get over 900 results. So that means manually checking and replacing 900 locations. Just blindly adding .AsEnumerable() via regex search+replace is not reliable enough as there is big risk of adding stuff in the wrong place (e.g. where the Contains is not within an ef expression).

@Suchiman
Copy link
Contributor

Suchiman commented Dec 19, 2024

But AFAIK there is not an easy way to reliably find and fix all occurencies automatically.

Set <LangVersion> to preview, then find a single occurence (or make one up if not trivially found), cursor to .Contains and then Shift + F12 (find all references), that will find all uses of MemoryExtensions.Contains, that doesn't strictly boil it down to all .Contains within LINQ but it should get you much closer.

@davidroth
Copy link
Contributor

davidroth commented Dec 19, 2024

Set to preview, then find a single occurence (or make one up if not trivially found), cursor to .Contains and then Shift + F12 (find all references), that will find all uses of MemoryExtensions.Contains, that doesn't strictly boil it down to all .Contains within LINQ but it should get you much closer.

Nice, that is a useful trick to narrow it down to the most relevant usages. Thanks. 👍

@roji roji changed the title InvalidOperationException when LangVersion is set to preview First-class span support causes translation failure with Contains Feb 14, 2025
@roji roji marked this as a duplicate of npgsql/efcore.pg#3461 Mar 3, 2025
@roji
Copy link
Member

roji commented Mar 3, 2025

Reopening to consider backporting to 9.0.x and 8.0.x.

@roji roji reopened this Mar 3, 2025
@roji roji removed this from the 10.0.0 milestone Mar 3, 2025
roji added a commit to roji/efcore that referenced this issue Mar 3, 2025
roji added a commit to roji/efcore that referenced this issue Mar 3, 2025
roji added a commit to roji/efcore that referenced this issue Mar 3, 2025
roji added a commit to roji/efcore that referenced this issue Mar 3, 2025
roji added a commit that referenced this issue Mar 4, 2025
@roji roji added this to the 8.0.x milestone Mar 4, 2025
@roji
Copy link
Member

roji commented Mar 4, 2025

Backported to 8.0 and 9.0.

@roji roji closed this as completed Mar 4, 2025
@ChrisJollyAU
Copy link
Contributor

@roji This has probably popped up only now as 9.0.20x SDK is linked to VS 17.13 (whereas the previously working 9.0.10x was VS 12.12). From https://github.com/dotnet/roslyn/blob/main/docs/compilers/CSharp/Compiler%20Breaking%20Changes%20-%20DotNet%2010.md#spant-and-readonlyspant-overloads-are-applicable-in-more-scenarios-in-c-14-and-newer it mentions it was introduced in VS 17.13 (and obviously together with the .20x SDK and in conjunction as the preview feature but still there). Those still on VS 17.12 wouldn't have hit this even with preview on

@roji
Copy link
Member

roji commented Mar 4, 2025

Thanks for the additional info @ChrisJollyAU. We discussed this as a team, and given how simple/low-risk the fix (or rather workaround) is, we decided it's worth making EF 8.0 and 9.0 forward-compatible with C# 14 by backporting it.

@ChrisJollyAU
Copy link
Contributor

Thanks for the additional info @ChrisJollyAU. We discussed this as a team, and given how simple/low-risk the fix (or rather workaround) is, we decided it's worth making EF 8.0 and 9.0 forward-compatible with C# 14 by backporting it.

True. Honestly, I'm more surprised/intrigued that you can turn a preview c# 14 feature on in a 9.0 stable release.

PS. Bit of extra reading I will link https://github.com/dotnet/csharplang/blob/main/meetings/2025/LDM-2025-01-06.md#ignoring-ref-structs-in-expressions. Was a proposal to ignore ref structs in expression trees. LDM decided not to make any changes

@roji
Copy link
Member

roji commented Mar 4, 2025

Honestly, I'm more surprised/intrigued that you can turn a preview c# 14 feature on in a 9.0 stable release.

I admit that I am too...

Was a proposal to ignore ref structs in expression trees. LDM decided not to make any changes

Yeah, I aware of this discussion... There have been several instances where first-class spans have caused trouble recently around expression trees and LINQ (see also this, which is a result of #35547).

roji added a commit to roji/efcore that referenced this issue Mar 4, 2025
@roji roji marked this as a duplicate of npgsql/npgsql#6039 Mar 5, 2025
roji added a commit that referenced this issue Mar 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants