【问题标题】:EF Core - Expression Tree Equivalent for IQueryable SearchEF Core - 等效于 IQueryable 搜索的表达式树
【发布时间】:2020-01-16 18:26:57
【问题描述】:

我有一个初始工作流程,允许我对 IQueryable 中包含的对象的字符串属性执行包容性搜索:

public static IQueryable ApplySearch(this IQueryable queryable, string search)
{
    // validation omitted for brevity
    var expression = queryable
        .Cast<object>()
        .Where(item => item.SearchStringTree(search))
        .Expression;

    var result = queryable.Provider.CreateQuery(expression);
    return result;
}

static bool SearchStringTree<T>(this T value, string search) =>
    value.GetObjectStrings().Any(s => s.Contains(search.ToLower()));

static IEnumerable<string> GetObjectStrings<T>(this T value)
{
    var strings = new List<string>();

    var properties = value.GetType()
        .GetProperties()
        .Where(x => x.CanRead);

    foreach (var prop in properties)
    {
        var t = prop.PropertyType.ToString().ToLower();
        var root = t.Split('.')[0];

        if (t == "system.string")
        {
            strings.Add(((string)prop.GetValue(value)).ToLower());
        }
        else if (!(root == "system"))
        {
            strings.AddRange(prop.GetValue(value).GetObjectStrings());
        }
    }

    return strings;
}

是否有可能以实体框架可以在 DbContext 执行之前进行转换的方式应用此概念?

我一直在研究可能使用 Expression Trees 来完成此任务。

这是一个有效的Repl.it,显示了上面的IQueryable 实现。

【问题讨论】:

    标签: c# .net-core entity-framework-core


    【解决方案1】:

    您肯定需要构建表达式树,基本上是所有(嵌套)string 属性的多个 or (C# ||) 谓词表达式。

    类似这样的东西(你的代码的表达式版本):

    public static class FilterExpression
    {
        public static IQueryable<T> ApplySearch<T>(this IQueryable<T> source, string search)
        {
            if (source == null) throw new ArgumentNullException(nameof(source));
            if (string.IsNullOrWhiteSpace(search)) return source;
    
            var parameter = Expression.Parameter(typeof(T), "e");
            // The following simulates closure to let EF Core create parameter rather than constant value (in case you use `Expresssion.Constant(search)`)
            var value = Expression.Property(Expression.Constant(new { search }), nameof(search));
            var body = SearchStrings(parameter, value);
            if (body == null) return source;
    
            var predicate = Expression.Lambda<Func<T, bool>>(body, parameter);
            return source.Where(predicate);
        }
    
        static Expression SearchStrings(Expression target, Expression search)
        {
            Expression result = null;
    
            var properties = target.Type
              .GetProperties()
              .Where(x => x.CanRead);
    
            foreach (var prop in properties)
            {
                Expression condition = null;
                var propValue = Expression.MakeMemberAccess(target, prop);
                if (prop.PropertyType == typeof(string))
                {
                    var comparand = Expression.Call(propValue, nameof(string.ToLower), Type.EmptyTypes);
                    condition = Expression.Call(comparand, nameof(string.Contains), Type.EmptyTypes, search);
                }
                else if (!prop.PropertyType.Namespace.StartsWith("System."))
                {
                    condition = SearchStrings(propValue, search);
                }
                if (condition != null)
                    result = result == null ? condition : Expression.OrElse(result, condition);
            }
    
            return result;
        }
    }
    

    非通用版本并没有太大的不同——只是你需要在查询表达式树中生成一个“调用”来代替 Where 扩展方法:

    public static IQueryable ApplySearch(this IQueryable source, string search)
    {
        if (source == null) throw new ArgumentNullException(nameof(source));
        if (string.IsNullOrWhiteSpace(search)) return source;
    
        var parameter = Expression.Parameter(source.ElementType, "e");
        var value = Expression.Property(Expression.Constant(new { search }), nameof(search));
        var body = SearchStrings(parameter, value);
        if (body == null) return source;
    
        var predicate = Expression.Lambda(body, parameter);
        var filtered = Expression.Call(
            typeof(Queryable), nameof(Queryable.Where), new[] { source.ElementType },
            source.Expression, Expression.Quote(predicate));
        return source.Provider.CreateQuery(filtered);
    }
    

    虽然这可行,但它并没有多大用处,因为所有 LINQ 扩展方法(包括 AsEnumerable(),ToList()` 等)都使用通用接口。

    同样在这两种情况下,查询元素的类型必须事先知道,例如T 在通用版本中,query.ElementType 在非通用版本中。这是因为表达式树是预先处理好的,当没有“对象”时,不能使用item.GetType()。出于同样的原因,IQueryable EF Core 等翻译器不喜欢 Cast 在查询表达式树中“调用”。

    【讨论】:

    • 感谢您的反馈!这可以与 Entity Framework Core 一起使用吗?当我在将IQueryable 数据加载到内存之前运行问题中的示例时,会抛出一个异常,基本上说 EF 无法将逻辑转换为 SQL。我希望它在从 DbContext 查询数据之前执行的主要原因是我希望它与分页和排序一起工作。
    • 我注意到的另一件事是您将ApplySearch 设为通用,但我正在使用没有类型信息的标准IQueryable(因此所有反射)。
    • (1) 是的,这种方法允许 EF Core 查询翻译 (2) 我注意到您使用非通用 IQueryable,但发现它有点奇怪,尤其是之前使用 Cast&lt;object&gt; 和 @987654339 @ 后。请注意,EF Core 不支持此类 Cast 调用。大多数情况下,您输入的 IQueryable 与标准 LINQ 一样。无论如何,如果你真的想让ApplySearch使用非泛型IQueryable,修改相对简单,因为主要工作是在寻找字符串属性和构建表达式的私有递归方法中。
    • 之所以使用非泛型IQueryable 是因为在ActionFilterAttribute.OnResultExecutionAsync 方法中从ResultExecutingContext.Result.Value 检索源。再次感谢你的帮助。与编写 LINQ 查询相比,表达式树语法有点吓人,但这绝对足以让我到达我需要的地方。
    猜你喜欢
    • 1970-01-01
    • 2017-05-30
    • 2022-01-18
    • 2010-12-07
    • 1970-01-01
    • 2020-06-12
    • 2015-08-20
    • 1970-01-01
    • 2011-02-11
    相关资源
    最近更新 更多