IQueryable<T> 高级 where 的扩展方法

IQueryable<T> extension method for advanced where

我正在尝试为 IQueryable<T> 添加扩展方法,以便将特定位置附加到 IQueryable

假设用户正在搜索名称等于 "Matt"

的数据
public IEnumerable<Employees> Search(String name)
{
    var qList = _context.Employees;
    if(!String.isNullOrEmpty(name))
        qList = qList.Where(emp => emp.Name == name);
    return qList.ToList();
}

现在,如果用户正在搜索名称以 "Matt" 开头的数据 - 他可能会尝试在特定文本框中写入 "Matt%**""Matt*" 之类的内容。预测这个我可以做到:

public IEnumerable<Employees> Search(String name)
{
    var qList = _context.Employees;
    if(!String.isNullOrEmpty(name))
    {
        string extractedName = name.Replace("%","").Replace("*","");
        if(name.EndsWith("%") || name.EndsWith("*");
            qList = qList.Where(emp => emp.Name.StartsWith(extractedName));
    }

    return qList.ToList();
}

此处将有很多 IF 语句来预测所有可能性,但还可以。我能做到。但我不喜欢的是每次执行此类功能时都重复我的代码。

如何为

制作扩展方法
IQueryable<T>

哪个会一直为我做这个检查?

public static IQueryable<T> MyAdvancedSearch<T>(this IQueryable<T> qList, String searchText)
{
    if(!String.isNullOrEmpty(searchText))
    /// ... Problem starts here.. because I cannot make this "Where": 
    qList.Where(prop=>prop.??? == searchText);
    /// ... I dont know how to tell my method which field should be filtered
}

更新 我已经设法创建了一个看起来不错的扩展方法:

        public static IQueryable<T> Filter<T>(this IQueryable<T> qList, Func<T,string> property, string text )
        {
            if (!String.IsNullOrEmpty(text))
            {
                if ((text.StartsWith("%") || text.StartsWith("*")) && (text.EndsWith("*") || text.EndsWith("*")))
                    qList = qList.Where(e => property(e).Contains(text));

                else if (text.StartsWith("%") || text.StartsWith("*"))
                    qList = qList.Where(e => property(e).EndsWith(text));

                else if (text.EndsWith("%") || text.EndsWith("*"))
                    qList = qList.Where(e => property(e).StartsWith(text));
                else
                    qList = qList.Where(e => property(e) == text);
            }
            return qList;
        }

但是当我尝试执行 qList.Tolist() 时,我收到错误消息:

InvalidOperationException: The LINQ expression 'DbSet<Employee>()
.Where(o => Invoke(__property_0, o)
== __text_1)' could not be translated.

InvalidOperationException: The LINQ expression 'DbSet<Employee>() .Where(o => Invoke(__property_0, o) == __text_1)' 
could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'. 
See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.

这应该可以解决问题

public static IQueryable<T> MyAdvancedSearch<T>(this IQueryable<T> qList, Expression<Func<T, string>> property, String text)
{
    Expression<Func<T, bool>> expression = null;

    var propertyName = GetPropertyName(property);
    var parameter = Expression.Parameter(typeof(T), "x");
    var left = propertyName.Split('.').Aggregate((Expression)parameter, Expression.Property);
    var right = Expression.Constant(text, typeof(string));
    Expression body = null;

    if (!string.IsNullOrEmpty(text))
    {
        if ((text.StartsWith("%") || text.StartsWith("*")) && (text.EndsWith("%") || text.EndsWith("*")))
            body = Expression.Call(left, "Contains", Type.EmptyTypes, Expression.Constant(text, typeof(string)));

        else if (text.StartsWith("%") || text.StartsWith("*"))
                body = Expression.Call(left, "EndsWith", Type.EmptyTypes, Expression.Constant(text, typeof(string)));

        else if (text.EndsWith("%") || text.EndsWith("*"))
                body = Expression.Call(left, "StartsWith", Type.EmptyTypes, Expression.Constant(text, typeof(string)));
        else
                body = Expression.MakeBinary(ExpressionType.Equal, left, right);
    }

    expression = Expression.Lambda<Func<T, bool>>(body, parameter);

    qList = qList.Where(expression);
        return qList;
}
public static string GetPropertyName<TClass, TProperty>(Expression<Func<TClass, TProperty>> property)
{
    MemberExpression member = property.Body as MemberExpression;
    PropertyInfo info = member.Member as PropertyInfo;

    return info.Name;
}
public static IQueryable<T> MyAdvancedSearch<T>(this IQueryable<T> qList, Func<T, string> func, String searchText)
        {
            qList.Where(prop => func(prop) == searchText);

            return qList;
        }
然后你可以这样称呼它:
var qList = _context.Employees.MyAdvancedSearch((emp) => emp.Name, extractedname)

该代码看起来您​​正在使用 Entity Framework,因此 LINQ 语句试图在幕后为您创建一个 SQL 查询。但是它无法从 'property(e)' 函数调用创建 SQL 语句。您可以先将 IQueryable 转换为列表以将数据提取到内存中。然后,LINQ 将停止尝试将查询转换为 SQL。不过不确定这样做对性能的影响。

以上使用 Func<> 的答案将不起作用,因为 Entity Framework(或任何 ORM)无法将委托分解为其组成部分以将其转换为 SQL。 IQueryable<> 是围绕 Expression API 设计的,这意味着您需要将 Func<,> 替换为 Expression<,>

但是,您不能像调用委托那样简单地调用 Expression,因此切换类型不能单独解决此问题t/won。相反,您可以 重写 表达式,使用辅助方法和 ExpressionVisitor 将您的逻辑转换为 Entity Framework 可以 解构并转换为SQL。我们的辅助方法将采用两个参数:

  • Expression<Func<TEntity, TProperty>> 表示有问题的 属性,
  • 代表过滤子句的Expression<TProperty, bool>

(TProperty 在我们的例子中将解析为 string)

我们将创建一个新的 Expression<Func<TEntity, bool>>,方法是将对第二个(过滤)表达式的 TPropertystring)参数的引用替换为 body 第一个 (属性) 表达式:

public static class QueryExtensions
{
    public static IQueryable<T> Filter<T>(
        this IQueryable<T> qList, 
        Expression<Func<T, string>> property, 
        string text)
    {
        
        var filterText = text?.Trim('%', '*');
        if (string.IsNullOrWhiteSpace(filterText))
            return qList;

        var matchEnd = text.StartsWith("*") || text.StartsWith("%");
        var matchStart = text.EndsWith("*") || text.EndsWith("%");
            
        if (matchEnd && matchStart)
            return qList.Where( TranslateFilter( property, e => e.Contains( filterText ) ) );
    
        if (matchEnd)
            return qList.Where( TranslateFilter( property, e => e.EndsWith( filterText ) ) );
            
        if (matchStart)
            return qList.Where( TranslateFilter( property, e => e.StartsWith( filterText ) ) );
            
        return qList.Where( TranslateFilter( property, e => e == text ) );
    }
    
    private static Expression<Func<TEntity, bool>> TranslateFilter<TEntity, TProperty>( 
        Expression<Func<TEntity, TProperty>> prop, 
        Expression<Func<TProperty, bool>> filter)
    {
        var newFilterExpression = new Visitor<TEntity>(prop).Visit(filter);
        return (Expression<Func<TEntity, bool>>) newFilterExpression;
    }
    
    private class Visitor<TEntity> : ExpressionVisitor
    {
        private readonly ParameterExpression _parameter;
        private readonly Expression _body;
        public Visitor(LambdaExpression prop)
        {
            _parameter = prop.Parameters[0];
            _body = prop.Body;
        }
    
        // return the body of the property expression any time we encounter
        // the parameter expression of the filter expression
        protected override Expression VisitParameter(ParameterExpression node) => _body;

        public override Expression Visit(Expression node)
        {
            if (node is LambdaExpression lamda)
            {
                // Visit the body of the filter lambda, replacing references to the string 
                // parameter with the body of the property expression
                var newBody = this.Visit(lamda.Body);
                
                // construct a new lambda expression with the new body and the original parameter
                return Expression.Lambda<Func<TEntity, bool>>(newBody, _parameter);
            }            
            return base.Visit(node);
        }
    }
}

与上面的答案不同,这将适用于 Entity Framework 支持的任何“特殊”功能——无需手动编写 starts/ends 等构建表达式的逻辑.

虽然我们通过扩展方法将它限制为 string,但上面的方法可以与任何类型的属性一起使用,例如 int。以下是一个示例,可以让您接受针对整列的文本过滤器,例如 >= 5!= 33

public static IQueryable<T> FilterNumber<T>(
    this IQueryable<T> qList, 
    Expression<Func<T, int>> property, 
    string filterQuery)
{
    if (string.IsNullOrEmpty(filterQuery))
        return qList;

    var match = Regex.Match(filterQuery.Trim(), "^(?<sym><=|<|>|>=|={1,2}|!=|<>)?\s?(?<num>\d+)$");
    if (!match.Success)
        throw new Exception();

    var number = int.Parse(match.Groups["num"].Value);
    
    // no symbol, assume exact match
    if (!match.Groups["sym"].Success)
        return qList.Where(TranslateFilter(property, c => c == number));

    return match.Groups["sym"].Value switch
    {
        "==" => qList.Where(TranslateFilter(property, c => c == number)),
        "="  => qList.Where(TranslateFilter(property, c => c == number)),
        "!=" => qList.Where(TranslateFilter(property, c => c != number)),
        "<>" => qList.Where(TranslateFilter(property, c => c != number)),
        "<"  => qList.Where(TranslateFilter(property, c => c <  number)),
        "<=" => qList.Where(TranslateFilter(property, c => c <= number)),
        ">"  => qList.Where(TranslateFilter(property, c => c >  number)),
        _    => qList.Where(TranslateFilter(property, c => c >= number))
    };
}

最终对我有用的版本:

public static IQueryable<T> MyAdvancedSearch<T>(this IQueryable<T> qList, Expression<Func<T, string>> property, String searchExpression)
        {

            if (string.IsNullOrEmpty(searchExpression))
                return qList;
            Expression<Func<T, bool>> expression = null;

            var propertyName = GetPropertyName(property);
            var parameter = Expression.Parameter(typeof(T), "x");
            var left = propertyName.Split('.').Aggregate((Expression)parameter, Expression.Property);
            var right = Expression.Constant(searchExpression, typeof(string));

            var pattern = new Regex("[*%]"); 
            string searchText = pattern.Replace(searchExpression, "");
            Expression body = null;
    
                if ((searchExpression.StartsWith("%") || searchExpression.StartsWith("*")) && (searchExpression.EndsWith("*") || searchExpression.EndsWith("*")))
                    body = Expression.Call(left, "Contains", Type.EmptyTypes, Expression.Constant(searchText, typeof(string)));

                else if (searchExpression.StartsWith("%") || searchExpression.StartsWith("*"))
                    body = Expression.Call(left, "EndsWith", Type.EmptyTypes, Expression.Constant(searchText, typeof(string)));

                else if (searchExpression.EndsWith("%") || searchExpression.EndsWith("*"))
                    body = Expression.Call(left, "StartsWith", Type.EmptyTypes, Expression.Constant(searchText, typeof(string)));
                else
                    body = Expression.MakeBinary(ExpressionType.Equal, left, right);
          

            expression = Expression.Lambda<Func<T, bool>>(body, parameter);

            qList = qList.Where(expression);
            return qList;
        }
        public static string GetPropertyName<TClass, TProperty>(Expression<Func<TClass, TProperty>> property)
        {
            MemberExpression member = property.Body as MemberExpression;
            var info = member.Member as PropertyInfo;
            return info.Name;
        }