开发者

How to turn this Func into an expression?

开发者 https://www.devze.com 2023-03-08 09:35 出处:网络
I am playing around with expression trees and trying to better understand how they work. I wrote some sample code that I\'m working with and hopefully someone can help me out.

I am playing around with expression trees and trying to better understand how they work. I wrote some sample code that I'm working with and hopefully someone can help me out.

So I have this somewhat messy query:

/// <summary>
/// Retrieves the total number of messages for the user.
/// </summary>
/// <param name="username">The name of the user.</param>
/// <param name="sent">True if retrieving the number of messages sent.</param>
/// <returns>The total number of messages.</returns>
public int GetMessageCountBy_Username(string username, bool sent)
{
    var que开发者_Go百科ry = _dataContext.Messages
        .Where(x => (sent ? x.Sender.ToLower() : x.Recipient.ToLower()) == username.ToLower())
        .Count();
    return query;
}

_dataContext is the entity framework data context. This query works beautifully, but it's not easy to read. I decided to factor the inline IF statement out into a Func like this:

public int GetMessageCountBy_Username(string username, bool sent)
{
    Func<Message, string> userSelector = x => sent ? x.Sender : x.Recipient;
    var query = _dataContext.Messages
        .Where(x => userSelector(x).ToLower() == username.ToLower())
        .Count();
    return query;
}

This seems like it would work great, but there is a problem. Because the query is against IQueryable<T> this LINQ expression is being translated into SQL to be executed at the data source. That's great, but because of this it does not know what to do with the call to userSelector(x) and throws an exception. It cannot translate this delegate into an expression.

So now that I understand why it's failing I would like to try and make it work. It's far more work for what I need, but I'm doing it just out of pure interest. How might I turn this Func into an expression that can be translated into SQL?

I tried to do this:

Expression<Func<Message, string>> userSelectorExpression = x => sent ? x.Sender : x.Recipient;
Func<Message, string> userSelector = userSelectorExpression.Compile();

With this however, I get the same error. I think I'm failing to understand expressions. I think all I'm doing with the above code is writing an expression but then turning it into executable code again and then getting the same error. However, if I try to use userSelectorExpression within the LINQ query it can't be called like a method.

Edit

For those interested in the exception, here it is:

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

I took this to mean that it could not "invoke" the userSelector delegate. Because, as stated above, it needs to translate it into an expression tree.

When using a real method, you get a slightly more verbose error message:

LINQ to Entities does not recognize the method 'System.String userSelector(Message, Boolean)' method, and this method cannot be translated into a store expression.


No need to complicate:

return sent
    ? _dataContext.Messages.Count(x => x.Sender.ToLower() == username.ToLower())
    : _dataContext.Messages.Count(x => x.Recipient.ToLower() == username.ToLower());


Well after playing around a bit, I got what I wanted.

This didn't save me tons of code in this case, but it does make the base query much easier to look at. For more complicated queries in the future this will be awesome! This query logic never gets repeated, but still gets re-used as many times as I need to.

First I have two methods in my repository. One counts the total number of messages (the one I used as the example in my question) and one that actually gets a collection of messages by page number. Here is how they are structured:

The one that gets a total count of messages:

    /// <summary>
    /// Retrieves the total number of messages for the user.
    /// </summary>
    /// <param name="username">The name of the user.</param>
    /// <param name="sent">True if retrieving the number of messages sent.</param>
    /// <returns>The total number of messages.</returns>
    public int GetMessageCountBy_Username(string username, bool sent)
    {
        var query = _dataContext.Messages
            .Count(UserSelector(username, sent));
        return query;
    }

The one that gets messages and pages them:

    /// <summary>
    /// Retrieves a list of messages from the data context for a user.
    /// </summary>
    /// <param name="username">The name of the user.</param>
    /// <param name="page">The page number.</param>
    /// <param name="itemsPerPage">The number of items to display per page.</param>
    /// <returns>An enumerable list of messages.</returns>
    public IEnumerable<Message> GetMessagesBy_Username(string username, int page, int itemsPerPage, bool sent)
    {
        var query = _dataContext.Messages
            .Where(UserSelector(username, sent))
            .OrderByDescending(x => x.SentDate)
            .Skip(itemsPerPage * (page - 1))
            .Take(itemsPerPage);
        return query;
    }

Obviously it is the call to UserSelector(string, bool) that is the big deal here. Here is what that method looks like:

    /// <summary>
    /// Builds an expression to be reused in a LINQ query.
    /// </summary>
    /// <param name="username">The name of the user.</param>
    /// <param name="sent">True if retrieving sent messages.</param>
    /// <returns>An expression to be used in a LINQ query.</returns>
    private Expression<Func<Message, bool>> UserSelector(string username, bool sent)
    {
        return x => ((sent ? x.FromUser : x.ToUser).Username.ToLower() == username.ToLower()) && (sent ? !x.SenderDeleted : !x.RecipientDeleted);
    }

So this method builds an expression to be evaluated and properly gets translated into it's SQL equivalent. The function in the expression evaluates to true if the username matches the username of either the sender or the recipient and deleted is false for either sender or recipient, based on the supplied boolean sent that gets serialized into the expression.

Here is a version of the above, that is closer to the example in my question. It's not as readable since my expression is grotesque but at lease I understand how it's working now:

    public int GetMessageCountBy_Username(string username, bool sent)
    {
        Expression<Func<Message, bool>> userSelector = x => ((sent ? x.FromUser : x.ToUser).Username.ToLower() == username.ToLower()) && (sent ? !x.SenderDeleted : !x.RecipientDeleted);

        var query = _dataContext.Messages
            .Count(userSelector);
        return query;
    }

This is actually pretty cool stuff. Took a lot of time to figure out but this seems really powerful. I now have a new understanding of how LINQ, lambdas, and expressions work :)

Thanks to everyone who contributed to this question! (including you artplastika, I still love you even if I don't love your answer)


Maybe this could be of use to help you abstract away conditions(predicates): http://www.albahari.com/nutshell/predicatebuilder.aspx

0

精彩评论

暂无评论...
验证码 换一张
取 消