2

Here is an example of EF Core 8 translating LINQ to embedded collections:

https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-8.0/whatsnew#translate-queries-into-embedded-collections

And it works just fine:

public class Person 
{
    public string Name { get; set; } = null!;
    public string Address { get; set; } = null!;
}
DbSet<Person> db;

var searchTerms = new[] { "Search #1", "Search #2", "Search #3"};

var linqQuery = db.Where(a => searchTerms.Contains(a.Address)).ToQueryString();

// DECLARE @__searchTerms_0 nvarchar(4000) = N'["Search #1","Search #2","Search #3"]';

// SELECT
//    [a].[Name], [a].[Address]
// FROM
//    [PERSON] AS[a]
// WHERE
//    [a].[Address] IN(
//    SELECT [s].[value]
//    FROM OPENJSON(@__searchTerms_0) WITH([value] varchar(8000) '$') AS[s]
// )

But I can't achieve the same result using an expression.

Here is what I try and expect to get OPENJSON:

DbSet<Person> db;

var searchTerms = new[] { "Search #1", "Search #2", "Search #3"};

var propInfo = typeof(Person).GetProperty(nameof(Person.Address))!;
var parameterExp = Expression.Parameter(typeof(Person), "a");

var method = typeof(Enumerable)
    .GetMethods()
    .First(n => n.Name == "Contains" && n.GetParameters().Length == 2);

var genericMethod = method.MakeGenericMethod(typeof(string));

var containsExp = Expression.Call(
   genericMethod, 
   Expression.Constant(searchTerms, typeof(IEnumerable<string>)), 
   Expression.Property(parameterExp, propInfo));

var predicate = Expression.Lambda<Func<Person, bool>>(containsExp, parameterExp);

var expressionQuery = db.Where(predicate).ToQueryString();

// SELECT
//    SELECT[a].[Name], [a].[Address]
// FROM
//    [PERSON] AS[a]
// WHERE
//    [a].[Address] IN(
//        'Search #1',
//        'Search #2',
//        'Search #3'
//    )
5
  • Enumerable<string>.Contains is already translated into "in (...)" have you tried with a complex type?
    – gordy
    Commented Feb 12 at 6:00
  • I don't need anything but string[]. It is translated, but not in IN( SELECT [s].[value] FROM OPENJSON(@__searchTerms_0) WITH([value] varchar(8000) '$') AS[s] )
    – Illia
    Commented Feb 12 at 8:58
  • a => searchTerms.Contains(a.Address) already is an expression. You get a different query because the expression you tried to create operator-by-operator doesn't match the initial one. Why are you doing this in the first place though? That why matters. LINQ is already generic and dynamically composable. You can write if (addressTerms.Length>-) { query=query.Where(a => searchTerms.Contains(a.Address));}. Are you trying to apply dynamic UI filters to your query? In that case you probably need to use LinqKit. Commented Feb 12 at 10:22
  • Yes, I get an XML filter file with field names, operations and values, which I need to apply when query DB. That's why I'm forced to use Expressions.
    – Illia
    Commented Feb 12 at 12:43
  • Does this answer your question? Entity Framework "SELECT IN" not using parameters
    – Servy
    Commented Mar 23 at 16:44

1 Answer 1

2

It is because in first variant you have used closure over searchTerms array - it is what C# compiler do, and in second variant you have used searchTerms array directly in expression tree as constant.

For constants EF Core just decided to generate static query with IN, because LINQ Translator makes decision that such query will be never changed.

We can mimic such case by defining fake holder class:

class ClosureHolder
{
    public IEnumerable<string> Value { get; set; }
}
DbSet<Person> db;

var searchTerms = new[] { "Search #1", "Search #2", "Search #3"};

var propInfo = typeof(Person).GetProperty(nameof(Person.Address))!;
var parameterExp = Expression.Parameter(typeof(Person), "a");

var method = typeof(Enumerable)
    .GetMethods()
    .First(n => n.Name == "Contains" && n.GetParameters().Length == 2);

var genericMethod = method.MakeGenericMethod(typeof(string));

// instead of generating just constant, we generate MemberExpression to constant
var holderExpr = Expression.Constant(new ClosureHolder { Value = searchTerms });
var searchTermsExpr = Expression.Property(holderExpr, nameof(ClosureHolder.Value));

var containsExp = Expression.Call(
   genericMethod, 
   searchTermsExpr, 
   Expression.Property(parameterExp, propInfo));

var predicate = Expression.Lambda<Func<Person, bool>>(containsExp, parameterExp);

var expressionQuery = db.Where(predicate).ToQueryString();
5
  • Thanks a lot! Just change the Value to be property instead of field
    – Illia
    Commented Feb 13 at 12:07
  • Nice trick, but just wondering: would it be possible to mimic the compiler's ability to create a FieldExpression from searchTerms directly? (That's what it does when compiling a => searchTerms.Contains(a.Address), but it's a field in a compiler-generated class). Commented Feb 13 at 12:55
  • 1
    @GertArnold, actually we mimic compiler's behavior almost in the same way. If we specify ClosureHolder<T> as generic, it will be replacement of compiler generated generic anonymous class. EF Core treats as parameters MemberExpression from ConstantExpression. Or maybe I do not understand your comment correctly :) Commented Feb 13 at 13:23
  • Understood correctly :) I think it's the best that can be done here, it would be a bit too much to emit a new class just to get a field. Commented Feb 13 at 13:41
  • 1
    It is what compiler do also. It instantiates closure class instance and puts into constant expression. No miracle and possible optimizations. What EF Core should do from their side - DO NOT cache this closure, otherwise memory will start grow. Commented Feb 13 at 13:47

Not the answer you're looking for? Browse other questions tagged or ask your own question.