-
Notifications
You must be signed in to change notification settings - Fork 804
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
Closure references are represented as constant nodes in expression trees #12841
Comments
It looks like this should be fixed in F# to improve Entity Framework interop. |
Finally found a workaround, just leave it here for others: npgsql/efcore.pg#2302 (comment) // wrap anything in a Some(...)
let ints = [| 1; 2 |] |> Some
// this will generate the correct behavior
db.Events.Where(fun x -> ints.Value.Contains(x.Id)).ToList() info: 3/15/2022 18:32:55.869 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand DbCommand (16ms) [Parameters=[@__Value_0={ '1', '2' } (DbType = Object)], CommandType='Text', CommandTimeout='30']
SELECT e."Id"
FROM "Events" AS e
WHERE e."Id" = ANY (@__Value_0) |
This shouldn't be marked as low priority since any efcore program written in fsharp will become vulnerable to sql injections, which is one of the highest possible vulnerabilities in any application. |
@Lanayx that is not true - EF Core properly escapes constants specified in the expression tree to prevent SQL injection. This affects the performance of the query (in a significant way!), but has no security implications. |
@roji thank you for the clarification, I can sleep well now :) |
There are some alternatives you might also like to try. Given this: open System.Linq
type R = { Data : int }
let db = [ for i in 1..100 -> { Data = i } ].AsQueryable()
Then I see this:
let i = 8
<@ fun x -> x.Data = i @> gives
type F3() =
let mutable i = 9
member x.Q = <@ fun x -> x.Data = i @> gives
let f2() =
let i = ref 8
<@ fun x -> x.Data = i.Value @>
f2() gives
type R<'T> = { Value: 'T }
let R x = { Value = x }
let f4() =
let i = R 8
<@ fun x -> x.Data = i.Value @>
f4() gives
|
I think any of (1)-(3) above should work to help make the parameterization structure of the quotation and query more explicit. I suspect (4) won't work. However EF may well be sensitive to each particular variation. |
@dsyme thanks for these suggestions. These indeed some like possible workarounds for the general problem, but I'm not sure they'd help in the EF Core case specifically. A very typical scenario is having a function with some parameter, which executes an EF that embeds that parameter inside the expression tree; top-level or module or class binding doesn't seem like it would work well there (unless users factor their code in ways that seem somewhat convulted). A reference call or boxing type could possibly work, but would involve considerable ceremony for what should ideally be a simple embedding of a parameter in a query. There would also be a pretty bad discoverability problem: even if a special boxing type exists, users would understandably assume that simply embedding a parameter directly is fine (as in C#), whereas in F# it would seem to work but lead to (severely) degraded performance. Is there any chance that the LINQ expression tree shape here could be different? I'm trying to make the EF (and possibly other LINQ providers) experience better in F#, hopefully that's possible. |
@roji The current F# behaviour is by-design. I don't think it can be changed. Please add a language suggestion for this. The only thing I can think of is to add a new attribute to put on parameters and locals to get them to be represented via a reference into a heap-allocated object. That will also be undiscoverable but perhaps EF samples can always show it. For comparison, here's some C#: public class A
{
static void F(System.Linq.Expressions.Expression<System.Func<int, int>> f)
{
Console.WriteLine("Hello, World!, f = {0}", f);
}
public static void M(int x)
{
F(a => x + 1);
}
public static void Main(string[] argv)
{
M(3);
}
} The output shows how
|
OK. Given that the expression tree shape won't change on the F# side, we're considering introducing an EF-specific mechanism for explicitly forcing parameterization in LINQ queries - EF.Param(). This may even be useful in some C# scenarios, see dotnet/efcore#28151. Am closing this issue. Until the EF-side support is done, use the ref workaround to make suer queries get parameterized properly, and please upvote dotnet/efcore#28151 to let the EF Core team know that you need it. |
Note that EF.Param has indeed been introduced on the EF side. |
An issue has been raised with using EF Core and F# that may point to a problem in the way F# generates LINQ expression trees.
In a nutshell, in the following query over an EF Core IQueryable, the
i
variable is represented by a simple ConstantExpression containing 8 (see full code sample below):The same code in C#:
... yields a very different expression tree: a FieldExpression is emitted for the closure object, with the object itself (the FieldExpression's Expression) being a ConstantExpression over a type that's identifiable as a closure (Attributes contains TypeAttributes.NestedPublic, and [CompilerGeneratedAttribute] is present).
In other words, the F# code above generates an expression that is identical to what's produced by the following C# code:
The distinction between a simple constant and a closure parameter is very important to EF Core: while a simple ConstantExpression gets embedded as-is in the SQL, a closure reference gets generated as a SQL parameter, which gets sent outside of the SQL (the SQL gets a placeholder such as
@i
). This has a profound effect on performance, as databases typically reuse query plans when the SQL is the same (with different parameters), but when different constants are embedded in the SQL, they do not.I realize things work quite differently in F#, and it may not necessarily be right for the exact same tree shape to be produced as in C#. However, the distinction itself (between constant and "closure parameter") is quite important.
/cc @NinoFloris @smitpatel @ajcvickers
F# runnable code sample
C# runnable code sample
The text was updated successfully, but these errors were encountered: