0

I am using the dictionary inside the IQueryable lambda linq throws the

Unable to create a constant value of type 'System.Collections.Generic.KeyValuePair`2

Code :

Dictionary<int, int> keyValues = new Dictionary<int, int>();

IQueryable<Account> = context.Account
    .Where(W => keyValues
        .Where(W1 => W1.Key == S.AccountID)
        .Where(W1 => W1.Value == S.Balance)
        .Count() > 0);

Details:

I have the data inside the dictionary like this

AccountID Balance

1 1000

2 2000

3 3000

I want the user which have the (ID = 1 AND Balance = 1000) OR (ID = 2 AND BALANCE = 2000) OR (ID = 3 AND BALANCE = 3000)

So how can I write the lambda for it ?

Edited

Thanks @caesay, Your answer help me lots.

I want one more favor from you.

From you answer I create the expression which look like below:

private static Expression<Func<Accounting, bool>> GenerateExpression(Dictionary<int, int> lstAccountsBalance)
{
    try
    {
        var objAccounting = Expression.Parameter(typeof(Accounting));
        Expression expr = null;
        const bool NOT_ALLOWED = false;

        if (lstAccountsBalance != null && lstAccountsBalance.Count > 0)
        {
            var clauses = new List<Expression>();

            foreach (var kvp in lstAccountsBalance)
            {
                clauses.Add(Expression.AndAlso(
                    Expression.Equal(Expression.Constant(kvp.Key), Expression.Property(objAccounting, nameof(Accounting.ID))),
                    Expression.Equal(Expression.Constant(kvp.Value), Expression.Property(objAccounting, nameof(Accounting.Balance)))
                ));
            }

            expr = clauses.First();
            foreach (var e in clauses.Skip(1))
            {
                expr = Expression.OrElse(e, expr);
            }

            var notAllowedExpr = Expression.AndAlso(
                    Expression.Equal(Expression.Constant(NOT_ALLOWED), Expression.Property(objAccounting, nameof(Accounting.ALLOWED))),
                    Expression.Equal(Expression.Constant(true), Expression.Constant(true))
                );

            expr = Expression.And(notAllowedExpr, expr);
        }

        var allowedExpr = Expression.AndAlso(
                Expression.Equal(Expression.Constant(!NOT_ALLOWED), Expression.Property(objAccounting, nameof(Accounting.ALLOWED))),
                Expression.Equal(Expression.Property(objAccounting, nameof(Accounting.ID)), Expression.Property(objAccounting, nameof(Accounting.ID)))
            );

        if (expr != null)
        {
            expr = Expression.OrElse(allowedExpr, expr);
        }
        else
        {
            expr = allowedExpr;
        }

        return Expression.Lambda<Func<Accounting, bool>>(expr, objAccounting);
    }
    catch (Exception ex)
    {
        throw objEx;
    }
}

After that I compiled the expression like this:

Expression<Func<Accounting, bool>> ExpressionFunctions = GenerateExpression(lstAccountsBalance);

var compiledExpression = ExpressionFunctions.Compile();

And I used like this:

.Select(S => new
{
    Accounting = S.Accounts
        .Join(context.AccountInfo,
                objAccounts => objAccounts.ID,
                objAccountInfo => objAccountInfo.ID,
                (objAccounts, objAccountInfo) => new Accounting
                {
                    ID = objAccounts.ID,
                    Balance = objAccountInfo.Balance,                               
                })
        .Where(W => W.ID == user.ID)
        .AsQueryable()
        .Where(W => compiledExpression(W))
        .Select(S1 => new Accounting()
        {
            ID = S1.ID,
            Balance = S1.Balance
        })
        .ToList(),
}

And it throws the exception with the message:

System.NotSupportedException: The LINQ expression node type 'Invoke' is not supported in LINQ to Entities.

Without Compile

Without the compile it works like charm. It gives the output want I want.

 Expression<Func<Accounting, bool>> ExpressionFunctions = GenerateExpression(lstAccountsBalance);

Use:

.Select(S => new
{
    Accounting = S.Accounts
        .Join(context.AccountInfo,
                objAccounts => objAccounts.ID,
                objAccountInfo => objAccountInfo.ID,
                (objAccounts, objAccountInfo) => new Accounting
                {
                    ID = objAccounts.ID,
                    Balance = objAccountInfo.Balance,                               
                })
        .Where(W => W.ID == user.ID)
        .AsQueryable()
        .Where(ExpressionFunctions)
        .Select(S1 => new Accounting()
        {
            ID = S1.ID,
            Balance = S1.Balance
        })

Thank you..

Kalpesh Rajai
  • 2,040
  • 27
  • 39
  • So what's the issue with the last sample? doesn't it do exactly what you want? What's the issue you're having with the last example? – caesay Mar 02 '17 at 12:49
  • I have the problem with the Performance. In my DB I have the 5,00,000 records and taking the 1,000 records from the table it takes 10 to 12 seconds. I want to improve the performance of the query I want decrease the time to 3 to 4 sec. – Kalpesh Rajai Mar 02 '17 at 13:17
  • This expression is being compiled to sql, and executed on your database server. Calling `Compile` here is transforming the expression tree into C# code - which can't be compiled to SQL anymore, even if it could it wouldn't speed anything up. The bottleneck here is your database server and generating the sql from this linq expression will be extremely fast already in comparison. – caesay Mar 02 '17 at 14:16
  • @caesay Thank you vary much. I am really appreciate your work. While Skip and Take to the last records like get data from the 4,99,000 and take 1,000 than it takes the 49 sec. It really vary slow. Any suggestion to improve this performance ? Thanks :) – Kalpesh Rajai Mar 02 '17 at 14:20
  • So the first step is to [find out what SQL that EF is generating](http://stackoverflow.com/questions/1412863/how-do-i-view-the-sql-generated-by-the-entity-framework), then run that in SQL Management Studio and profile it to see what part of the query is taking the longest. Maybe you're missing an index in the database that can speed things up. Try this out and if you don't have any luck open a new question that is performance related. – caesay Mar 02 '17 at 14:23

1 Answers1

1

Everything inside of a EF linq query needs to be compiled to an Expression tree, and then to SQL, but the dictionary is an IEnumerable so there is no way that EF could know how to compile that.

You can either build the expression tree yourself, or use the System.Linq.Dynamic nuget package to build the sql yourself.

The example using System.Linq.Dynamic, first install the nuget package and add the using to the top of your file, then there will be a Where overload that takes a string as a parameter:

context.Account.Where(
    String.Join(" OR ", keyValues.Select(kvp => $"(ID = {kvp.Key} AND Balance = {kvp.Value})")));

Essentially, everything inside of the Where clause will be executed directly as SQL, so be careful to use the Where(string, params object[]) overload to paramaterize your query if accepting user input.

The expression tree (untested) approach might look like the following:

var account = Expression.Parameter(typeof(Account));
var clauses = new List<Expression>();
foreach(var kvp in keyValues)
{
    clauses.Add(Expression.AndAlso(
        Expression.Equal(Expression.Constant(kvp.Key), Expression.Property(account, nameof(Account.AccountID))),
        Expression.Equal(Expression.Constant(kvp.Value), Expression.Property(account, nameof(Account.Balance)))
    ));
}

var expr = clauses.First();
foreach (var e in clauses.Skip(1))
    expr = Expression.OrElse(e, expr);

context.Account.Where(Expression.Lambda<Func<Account, bool>>(expr, account));

Essentially inside the foreach loop we're creating all of the (ID = ... AND Balance = ...) and then at the end we join them all with an OR.

caesay
  • 16,932
  • 15
  • 95
  • 160
  • Thank you @caesay, I have the two table and I am joining it using the `.Join` So one field is coming from the 1 table and another from the 2 table. – Kalpesh Rajai Mar 01 '17 at 10:14
  • @KalpeshRajai: If what I've provided doesn't give you enough to make your own solution then you'll need to update your question. Also if EF doesn't fit your needs, you can just execute raw sql for this using `context.Set().SqlQuery(...)` – caesay Mar 01 '17 at 10:41
  • Thanks, I modify some your code and it help me lot. I have one question that how can I pass expression only `Where(Expression.Lambda>(expr, account))` When value of some filed is specified. – Kalpesh Rajai Mar 01 '17 at 12:15
  • For example one filed called "Flg" is true than and only than I want the lambda to execute this expression otherwise not ? Please let me know how can I do that. Thanks a lot. – Kalpesh Rajai Mar 01 '17 at 12:17
  • I created expression based on your example. Now, It's working fine. May I know which Expression is better for the performance compiled expression or un-complied expression. Sorry, To disturb you can you please help ? How can I compile the Expression and use it in the Lambda. I tired the following `var finalExpr = Expression.Lambda>(expr, objMFPRoleOrgFun).Compile();` And I used it in the lambda but it throws the exception and without compile it can not throw the exception `.Where(W => finalExpr(W))` – Kalpesh Rajai Mar 01 '17 at 14:50
  • @KalpeshRajai: You can't compile / use it within a lambda expression like a method - once you compile it its just a regular method. The point of constructing a expression tree like this is that EntityFramework can analyse it and construct SQL based on it. – caesay Mar 01 '17 at 17:54
  • @KalpeshRajai: The expression tree in my example *is* a lambda expression, just un-compiled. If you need to change how it works, you should do it by modifying the expression itself. Can you explain what you're trying to accomplish by editing the question? – caesay Mar 01 '17 at 17:55
  • Thank you @caesay, I updated the question please can you help please help me to solve the problem. – Kalpesh Rajai Mar 02 '17 at 05:52