tabs ↹ over ␣ ␣ ␣ spaces

by Jiří {x2} Činčura

Playing with parameters limit on SQL Server with Entity Framework

5 Sep 2017 .NET, C#, Entity Framework, MS SQL Server

During a break of my user group talk yesterday I got a question about how Entity Framework translates Contains into an IN clause. Whether it uses parameters and how it handles limits that SQL Server has around the parameter count.

Setup

Let’s start with trivial model - Person and Department entities and simple DbContext.

[DbConfigurationType(typeof(MyContextConfiguration))]
public class MyContext : DbContext
{
    public MyContext()
        : base(@"Data Source=.\sqlexpress;Initial Catalog=demo;Integrated Security=True;MultipleActiveResultSets=True")
    { }

    public DbSet<Person> People { get; set; }
    public DbSet<Department> Departments { get; set; }
}

public class MyContextConfiguration : DbConfiguration
{
    public MyContextConfiguration()
    {
        SetDatabaseInitializer<MyContext>(null);
    }
}

public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual Department Department { get; set; }
    public int DepartmentId { get; set; }
}

public class Department
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Person> People { get; set; }
}

Regular LINQ

With that in place I can do simple query…

using (var db = new MyContext())
{
    db.Database.Log = Console.WriteLine;

    var ids = Enumerable.Range(1, 20).ToArray();
    db.People.Where(x => ids.Contains(x.DepartmentId)).ToList();
}

...which results in this SQL.

SELECT
    [Extent1].[Id] AS [Id],
    [Extent1].[Name] AS [Name],
    [Extent1].[DepartmentId] AS [DepartmentId]
    FROM [dbo].[People] AS [Extent1]
    WHERE [Extent1].[DepartmentId] IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)

No luck. The IN clause is there, but Entity Framework is using the values directly (and escaping these, i.e. in case it’s a string). Looks like it’s time to pull out some tricks.

Generating an expression tree

And what’s a better trick than creating expression trees yourself? Well, I could write the whole or-ing manually, but that’s not fun, is it?

using (var db = new MyContext())
{
    db.Database.Log = Console.WriteLine;

    var closure = new Closure() { Id = 10 };
    var personType = typeof(Person);
    var personMember = personType.GetProperty(nameof(Person.DepartmentId));
    var closureType = typeof(Closure);
    var closureMember = closureType.GetProperty(nameof(Closure.Id));
    var p = Expression.Parameter(personType);
    var equalExpression = Expression.Equal(
            Expression.MakeMemberAccess(p, personMember),
            Expression.MakeMemberAccess(Expression.Constant(closure, closureType), closureMember));
    var expr = equalExpression;
    for (int i = 0; i < 2098; i++)
    {
        expr = Expression.OrElse(expr, equalExpression);
    }
    var predicate = (Expression<Func<Person, bool>>)Expression.Lambda(expr, p);
    db.People.Where(predicate).ToList();
}

The expression is not difficult to create. I first create the Expression.Equal part, which corresponds to Person.DepartmentId == Closure.Id roughly. Then I take this piece and glue it together 2098 times with Expression.OrElse. The Closure class is really a closure I had to manually create for integer variable I’m using in the equality expression.

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

When you’re writing lambdas directly this is done for you automatically - you can spot something like <>c__DisplayClassX in i.e. debugger.

Running this code generates SQL, that contains 2099 parameters.

SELECT
    [Extent1].[Id] AS [Id],
    [Extent1].[DepartmentId] AS [DepartmentId],
    [Extent1].[Name] AS [Name]
    FROM [dbo].[People] AS [Extent1]
    WHERE [Extent1].[DepartmentId] IN (@p__linq__0,@p__linq__1,@p__linq__2,
...
@p__linq__2096,@p__linq__2097,@p__linq__2098)

This finally makes SQL Server unhappy and results in an exception telling me The incoming request has too many parameters. The server supports a maximum of 2100 parameters. Reduce the number of parameters and resend the request.. Although I have only 2099. Probably the message means < 2100 and not <= 2100 which is how I understand it intuitively.

Conclusion

This was a nice play with expressions. And as I’m finishing writing this post I’m thinking whether the fact that Entity Framework generates the IN clause with values, and goes into the hassle of escaping, instead of parameters has something to do with this limit. 🤔