EF Core - Эквивалент дерева выражений для IQueryable Search - PullRequest
2 голосов
/ 15 января 2020

У меня есть исходный рабочий процесс, который позволяет мне выполнять инклюзивный поиск строковых свойств объектов, содержащихся в 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;
}

Возможно ли применить эту концепцию таким образом, чтобы Entity Framework может переводить до выполнения DbContext?

Я искал потенциальное использование деревьев выражений для достижения sh этого.

Вот рабочий Repl.it показывает реализацию IQueryable выше.

1 Ответ

2 голосов
/ 15 января 2020

Вам определенно нужно построить дерево выражений, в основном мульти or (C# ||) выражение предиката для всех (вложенных) string свойств.

Примерно так (версия выражения вашего 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;
    }
}

Версия non generi c не сильно отличается - просто вместо 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 () `et c.) Работают с универсальным интерфейсом c.

Также в обоих случаях тип элемента запроса должен быть известен заранее, например, T в версии generi c, query.ElementType в версии non generi c. Это связано с тем, что дерево выражений обрабатывается заранее, когда нет «объектов», поэтому оно не может использовать item.GetType(). По той же причине IQueryable переводчикам, таким как EF Core, не нравятся Cast "звонки" внутри дерева выражений запросов.

...