Skip to content

Closure references are represented as constant nodes in expression trees  #12841

@roji

Description

@roji

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):

let i = 8
db.Events.Where(fun x -> x.Data = i).ToList()

The same code in C#:

var i = 8;
_ = db.Events.Where(x => x.Data == i).ToList();

... 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:

_ = db.Events.Where(x => x.Data == 8).ToList();

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
open System
open System.Linq
open Microsoft.EntityFrameworkCore
open Microsoft.Extensions.Logging

#nowarn "20"

type Event() =
    member val Id: Guid = Guid.Empty with get, set
    member val Data: int = 0 with get, set

type ApplicationDbContext() =
    inherit DbContext()

    override this.OnConfiguring(builder) =
        builder
            .UseSqlServer(@"Server=localhost;Database=test;User=SA;Password=Abcd5678;Connect Timeout=60;ConnectRetryCount=0;Encrypt=false")
            .LogTo(Action<string>(Console.WriteLine), LogLevel.Information)
            .EnableSensitiveDataLogging() |> ignore

    [<DefaultValue>]
    val mutable private events: DbSet<Event>

    member this.Events
        with public get () = this.events
        and public set v = this.events <- v

[<EntryPoint>]
let main args =
    use db = new ApplicationDbContext()

    db.Database.EnsureDeleted()
    db.Database.EnsureCreated()

    let i = 8
    db.Events.Where(fun x -> x.Data = i).ToList()

    0
C# runnable code sample
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

await using var db = new BlogContext();
await db.Database.EnsureDeletedAsync();
await db.Database.EnsureCreatedAsync();

var i = 8;
_ = db.Events.Where(x => x.Data == i).ToList();

public class BlogContext : DbContext
{
    public DbSet<Event> Events { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseSqlServer(@"Server=localhost;Database=test;User=SA;Password=Abcd5678;Connect Timeout=60;ConnectRetryCount=0;Encrypt=false")
            .LogTo(Console.WriteLine, LogLevel.Information)
            .EnableSensitiveDataLogging();
}

public class Event
{
    public Guid Id { get; set; }
    public int Data { get; set; }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    Area-QuotationsQuotations (compiler support or library). See also "queries"BugImpact-Low(Internal MS Team use only) Describes an issue with limited impact on existing code.

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions