使用 Func 属性 选择器和 Entity Framework 6

Using Func property selector with Entity Framework 6

我正在尝试编写一个搜索函数,我可以在其中传递一个允许我指定要搜索的字符串 属性 的 Func。 Entity Framework 6 抱怨我正在尝试调用不允许的函数。是否有重写以下内容以便我可以将其与 EF 一起使用?

public List<Person> SearchPeople(string searchTerm, Func<Person, string> selector)
{                         
     return myDbContext.Persons.Where(person => selector(person).Contains(searchTerm)).ToList();                
}

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

您可以使用 LinqKit library to solve your issue (Check this link and this 获取更多信息。

public List<Person> SearchPeople(string searchTerm, Expression<Func<Person, string>> selector)
{                         
     return myDbContext.Persons
                       .AsExpandable()
                       .Where(person => selector.Invoke(person).Contains(searchTerm))
                       .ToList();                
}

AsExpandable 方法在 DLINQ Table 对象周围创建了一个薄包装器。多亏了这个包装器,您可以使用名为 Invoke 的第二种方法,它扩展了 Expression class 来调用 lambda 表达式,同时仍然可以将查询转换为 T-SQL。这是有效的,因为当转换为表达式树时,包装器用调用的 lambda 表达式的表达式树替换所有出现的 Invoke 方法,并将这些表达式传递给能够将扩展查询转换为的 DLINQ T-SQL.

所以您唯一需要做的就是将 SearchPeople 方法的第二个参数类型更改为 Expression<Func<Person,string>>,然后您可以调用您的方法,如下所示:

Expression<Func<Person,string>> exp= person=>person.Name;
//...
var people=SearchPeople("Albert",exp);

您尝试做的事情实际上比看起来更复杂,因为它涉及手动组合 Person 属性 表达式和表示对 string.Contains(searchTerm) 的调用的表达式。

当你可以直接写出完整的lambda表达式时

p => p.FirstName.Contains(searchTerm)

...一点也不难,因为编译器会为您完成所有繁重的工作。但是当你必须手动组合表达式片段时,就像在这种情况下,它很快就会变得混乱。

我还没有尝试过 octavioccl 的答案,但如果它像宣传的那样工作,那将是一个非常优雅的解决方案,如果可能的话我肯定会使用它。

但是,如果有的话,为了对编译器为我们所做的所有工作表示赞赏,这里是您可以在没有任何外部库的情况下解决您的问题的方法(使用一些 C# 6 功能,但可以很容易地对其进行调整对于旧版本):

public List<Person> SearchPeople(string searchTerm, Expression<Func<Person, string>> personProperty)
{
    // Note: Explanatory comments below assume that the "personProperty" lambda expression is:
    //       p => p.FirstName

    // Get MethodInfo for "string.Contains(string)" method.
    var stringContainsMethod = typeof(string).GetMethod(
        nameof(string.Contains),
        new Type[] { typeof(string) });

    // Create a closure class around searchTerm.
    // Using this technique allows EF to use parameter binding
    // when building the SQL query.
    // In contrast, simply using "Expression.Constant(searchTerm)",
    // makes EF hard-code the string in the SQL, which is not usually desirable.
    var closure = new { SearchTerm = searchTerm };
    var searchTermProperty = Expression.Property(
        Expression.Constant(closure), // closure
        nameof(closure.SearchTerm));  // SearchTerm

    // This forms the complete statement: p.FirstName.Contains(closure.SearchTerm)
    var completeStatement = Expression.Call(
        personProperty.Body,  // p.FirstName
        stringContainsMethod, // .Contains()
        searchTermProperty);  // closure.SearchTerm

    // This forms the complete lambda: p => p.FirstName.Contains(closure.SearchTerm)
    var whereClauseLambda = Expression.Lambda<Func<Person, bool>>(
        completeStatement,             // p.FirstName.Contains(closure.SearchTerm)
        personProperty.Parameters[0]); // p

    // Execute query using constructed lambda.
    return myDbContext.Persons.Where(whereClauseLambda).ToList();
}

然后您可以像这样调用该方法:

foreach (var person in SearchPeople("joe", p => p.FirstName))
{
    Console.WriteLine($"First Name: {person.FirstName}, Last Name: {person.LastName}");
}