简体   繁体   English

EF Core - 等效于 IQueryable 搜索的表达式树

[英]EF Core - Expression Tree Equivalent for IQueryable Search

I have an initial workflow put together that allows me to perform inclusive searches for string properties of objects contained in an IQueryable :我有一个初始工作流放在一起,它允许我对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;
}

Would it be possible to apply this concept in a way that Entity Framework can translate prior to DbContext execution?是否有可能以 Entity Framework 可以在 DbContext 执行之前转换的方式应用这个概念?

I've been looking into potentially using Expression Trees to accomplish this.我一直在研究可能使用表达式树来实现这一点。

Here's a working Repl.it showing the IQueryable implementation above.这是一个工作Repl.it,显示了上面的IQueryable实现。

You definitely need to build expression tree, basically multi or (C# || ) predicate expression for all (nested) string properties.您肯定需要为所有(嵌套) string属性构建表达式树,基本上是 multi or (C# || ) 谓词表达式。

Something like this (expression version of your code):像这样(代码的表达式版本):

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;
    }
}

The non generic version is not much different - just instead of Where extension method you need to generate a "call" to it in the query expression tree:非通用版本没有太大不同 - 只是代替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);
}

While this works, it's not much useful because all LINQ extensions methods (including AsEnumerable(), ToList()` etc.) work with generic interface.虽然这有效,但它没有多大用处,因为所有 LINQ 扩展方法(包括AsEnumerable(), ToList()` 等)都使用通用接口。

Also in both cases, the type of the query element must be known in advance, eg T in the generic version, query.ElementType in the non generic version.同样在这两种情况下,查询元素的类型必须事先知道,例如通用版本中的T ,非通用版本中的query.ElementType This is because expression tree are processed in advance, when there are no "objects", hence it can't use item.GetType() .这是因为表达式树是预先处理的,当没有“对象”时,因此它不能使用item.GetType() For the same reason, IQueryable translators like EF Core don't like Cast "calls" inside the query expression tree.出于同样的原因,像 EF Core 这样的IQueryable翻译器不喜欢在查询表达式树中Cast “调用”。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM