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

InvalidOperationException when LangVersion is set to preview #35100

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

InvalidOperationException when LangVersion is set to preview #35100

jamesgurung opened this issue Nov 13, 2024 · 38 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 claerly 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.

@pinkfloydx33
Copy link

I have my doubts if it can be wrapped in a convert to object call

It's illegal to box a ref struct, the compiler will block it in ordinary code. Convert(<exp>, typeof(object)) is essentially doing that. Also this seems to be an issue with the interpreter handling of spans. See commented portions SharpLab


Inlining arrays instead of using them as parameters I think would be a big deal. We heavily rely on them being parameters when working with Npgsql.

@ChrisJollyAU
Copy link
Contributor

ChrisJollyAU commented Nov 17, 2024

I'll give you an example in NorthwindMiscellaneousQuerySqlServerTest from Contains_over_concatenated_column_and_constant

Original query
``
var data = new[] { "ALFKI" + "SomeConstant", "ANATR" + "SomeConstant", "ALFKI" + "X" };

return AssertQuery(
async,
ss => ss.Set().Where(c => data.Contains(c.CustomerID + "SomeConstant")));
``

C# 13 sql

@__data_0='["ALFKISomeConstant","ANATRSomeConstant","ALFKIX"]' (Size = 4000)

SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region]
FROM [Customers] AS [c]
WHERE [c].[CustomerID] + N'SomeConstant' IN (
    SELECT [d].[value]
    FROM OPENJSON(@__data_0) WITH ([value] nvarchar(max) '$') AS [d]
)

C# 14 sql

SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region]
FROM [Customers] AS [c]
WHERE [c].[CustomerID] + N'SomeConstant' IN (N'ALFKISomeConstant', N'ANATRSomeConstant', N'ALFKIX')

Key thing to note is that data in C# 14 is not a Queryable so none of your Queryable pipeline runs
C# 13 effectively has data.AsQueryable().Contains where Contains is from IQueryable by the time you get to it
C# 14 is just data.Contains where Contains is from MemoryExtensions

@ChrisJollyAU
Copy link
Contributor

@roji @pinkfloydx33 To get back to the specific error from the OP (and not the overall first-class span types in general)

Looks like there is already issue with the runtime dotnet/runtime#96160

To quote one comment from nearly a year ago

The ref struct support for the expression tree interpreter would be a new major feature that conflicts with System.Linq.Expression status as effectively archived library.

Using preferInterpretation: false does work fine though

@roji
Copy link
Member

roji commented Nov 17, 2024

Using preferInterpretation: false does work fine though

We can certainly do that, though that would slow query execution for everyone.

Looks like there is already issue with the runtime dotnet/runtime#96160

Thanks for finding that! We should post on that issue to make sure they're aware of the interaction with first-class span type feature...

@stephentoub
Copy link
Member

stephentoub commented Dec 10, 2024

@jaredpar, @jjonescz, @333fred

@ChrisJollyAU
Copy link
Contributor

@roji Have you had a chance to look at my draft PR yet?

@roji
Copy link
Member

roji commented Dec 11, 2024

@ChrisJollyAU no, sorry about that - things have just been too busy lately. I'm planning a PR work phase soon though - so expect some reviewing.

@davidroth
Copy link
Contributor

davidroth commented Dec 19, 2024

@ajcvickers @roji Does this Span breaking change also affect classic EF6? If so, will classic EF6 be updated to support it, or is this now the dead end for using classic EF6 with modern .NET?

@ChrisJollyAU
Copy link
Contributor

@davidroth I think it would only be an issue if you are trying to use classic EF6 in a project that is being compiled with c# 14 and greater

Can always work around by dropping to c# 13 or explicitly cast to Enumerable to use the static extensions as seen in this doc dotnet/docs#43952

@davidroth
Copy link
Contributor

@ChrisJollyAU Yes, I think so too. But i doubt the EF6 query pipeline will sucessfully process after casting to Enumerable. Which basically confirms my assumption that this is literally a dead end for "EF6" if you want to continue using the latest dotnet and lang versions.

In our case, we could not (yet) upgrade our huge LOB EF6 application to EFCore, but we have successfully migrated to the latest modern .NET. With this break we are stuck with C#13 until we are able to migrate to EFCore (which is a big task given the complexity and size of this project).

@ChrisJollyAU
Copy link
Contributor

But i doubt the EF6 query pipeline will sucessfully process after casting to Enumerable

Don't see why it wouldnt work. Using the static Enumerable method should just force it to use the normal way rather than adding the implicit convert to a Span. So there wouldn't be any ref structs in the query that classic EF6 is processing.

Also, you still have time to test and investigate. C# 14 is still early in its previews will be out at the same time as .Net 10 in November 2025

@Suchiman
Copy link
Contributor

Suchiman commented Dec 19, 2024

Yes, I think so too. But i doubt the EF6 query pipeline will sucessfully process after casting to Enumerable

Only arrays will be auto converted to Span AFAIK, so if you use Where(x => list.Contains(x)) or Where(x => array.AsEnumerable().Contains(x)) then even EF6 should be able to deal with this.

@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. 👍

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

8 participants