Как создать универсальный метод для перебора полей объекта и использовать его в качестве предиката Where? - PullRequest
0 голосов
/ 25 ноября 2018

Я создаю универсальный интерфейс для предоставления выбранных свойств строки из класса, а затем я хочу найти текст внутри каждого из этих полей, чтобы проверить, соответствует ли он.

ВотМой IFieldExposer интерфейс:

using System;
using System.Collections.Generic;

public interface IFieldExposer<T>
{
  IEnumerable<Func<T, string>> GetFields();
}

Теперь я реализую это в моем DataClass, чтобы раскрыть свойства, которые я хотел бы повторить.Обратите внимание, что я также выставляю свойство из моего ChildClass:

using System;
using System.Collections.Generic;

class DataClass : IFieldExposer<DataClass>
{
  public string PropertyOne { get; set; }
  public string PropertyTwo { get; set; }
  public ChildClass Child { get; set; }

  public IEnumerable<Func<DataClass, string>> GetFields()
  {
    return new List<Func<DataClass, string>>
      {
        a => a.PropertyOne,
        b => b.Child.PropertyThree
      };
  }
}

class ChildClass
{
  public string PropertyThree { get; set; }
}

Я также создал методы расширения для IFieldExposer<T>, потому что я хочу сделать его простым и иметь возможность просто вызывать obj.Match(text, ignoreCase) везде в моем коде.Этот метод должен сказать мне, если мой объект соответствует моему тексту.Вот код для ExtensionClass, который не работает должным образом:

using System;
using System.Linq.Expressions;
using System.Reflection;

public static class ExtensionClass
{
  public static bool Match<T>(this IFieldExposer<T> obj, string text, bool ignoreCase)
  {
    Func<bool> expression = Expression.Lambda<Func<bool>>(obj.CreateExpressionTree(text, ignoreCase)).Compile();
    return expression();
  }

  private static Expression CreateExpressionTree<T>(this IFieldExposer<T> obj, string text, bool ignoreCase)
  {
    MethodInfo containsMethod = typeof(string).GetMethod("Contains", new Type[] { typeof(string) });

    var exposedFields = obj.GetFields();

    if (ignoreCase)
    {
      // How should I do convert these to lower too?
      // exposedFields = exposedFields.Select(e => e.???.ToLower());
      text = text.ToLower();
    }

    Expression textExp = Expression.Constant(text);
    Expression orExpressions = Expression.Constant(false);

    foreach (var field in exposedFields)
    {
      //How should I call the contains method on the string field?
      Expression fieldExpression = Expression.Lambda<Func<string>>(Expression.Call(Expression.Constant(obj), field.Method)); //this doesn't work
      Expression contains = Expression.Call(fieldExpression, containsMethod, textExp);
      orExpressions = Expression.Or(orExpressions, contains);
    }

    return orExpressions;
  }
}

Пожалуйста, проверьте комментарии в коде выше.Я хотел бы знать, как преобразовать все мои строковые свойства в нижний регистр (при желании) и как вызвать string.Contains в каждом из них.Я получаю эту ошибку при создании своего fieldExpression:

Method 'System.String <GetFields>b__12_0(DataClass)' declared on type 'DataClass+<>c' cannot be called with instance of type 'DataClass'

У меня нет опыта работы с деревьями выражений.Я часами читал документы и другие ответы на подобные вопросы, но все еще не могу понять, как добиться того, чего я хочу ... Я понятия не имею, что делать сейчас.

Я тестирую это вконсольное приложение, так что вот основной класс, если вы хотите создать его самостоятельно:

using System.Collections.Generic;
using System.Linq;

class Program
{
  static void Main(string[] args)
  {
    var data = new DataClass
    {
      PropertyOne = "Lorem",
      PropertyTwo = "Ipsum",
      Child = new ChildClass
      {
        PropertyThree = "Dolor"
      }
    };

    var dataList = new List<DataClass> { data };
    var results = dataList.Where(d => d.Match("dolor", true));

  }
}

РЕДАКТИРОВАТЬ

Я забыл упомянуть, что мой dataList должен быть IQueryable, и я хочувыполнить мой код в SQL, поэтому я пытаюсь построить деревья выражений самостоятельно.Итак, похоже, мой пример кода должен быть:

var dataList = new List<DataClass> { data };
var query = dataList.AsQueryable();
var results = query.Where(ExtensionClass.Match<DataClass>("lorem dolor"));

, в то время как мой метод становится: (Я слежу за ответом @ sjb-sjb и изменил метод GetFields() в IFieldExposer<T> на свойство SelectedFields)

public static Expression<Func<T, bool>> Match<T>(string text, bool ignoreCase) where T : IFieldExposer<T>
{
  ParameterExpression parameter = Expression.Parameter(typeof(T), "obj");
  MemberExpression selectedFieldsExp = Expression.Property(parameter, "SelectedFields");
  LambdaExpression lambda = Expression.Lambda(selectedFieldsExp, parameter).Compile();

  [...]

}

А потом кажется, что мне нужно динамически звонить selectedFieldsExp с Expression.Lambda.Я придумал:

Expression.Lambda(selectedFieldsExp, parameter).Compile();

, и это работает, но я не знаю, как правильно вызвать DynamicInvoke() для лямбда-выражения.

Выдает Parameter count mismatch., если я звонюэто без параметров и Object of type 'System.Linq.Expressions.TypedParameterExpression' cannot be converted to type 'DataClass'. если я делаю DynamicInvoke(parameter).

Есть идеи?

Ответы [ 3 ]

0 голосов
/ 26 ноября 2018

Прежде чем приступить к реализации, необходимо устранить некоторые недостатки проектирования.

Во-первых, почти все поставщики запросов (кроме LINQ to Object, который просто компилирует лямбда-выражения для делегатов и выполняет их)не поддерживает выражения вызова и пользовательские (неизвестные) методы.Это потому, что они не выполняют выражения, а переводят их во что-то другое (например, в SQL), а перевод основан на предварительных знаниях.

Одним из примеров выражения вызова являются Func<...> делегаты.Итак, первое, что вы должны сделать, это использовать Expression<Func<...>> везде, где у вас есть Func<...>.

Во-вторых, деревья выражений запросов строятся статически, то есть не существует экземпляра реального объекта, который вы можете использовать для полученияметаданные, поэтому идея IFieldExposer<T> не будет работать.Вам потребуется статически представленный список выражений, подобных этому:

class DataClass //: IFieldExposer<DataClass>
{
    // ...

    public static IEnumerable<Expression<Func<DataClass, string>>> GetFields()
    {
        return new List<Expression<Func<DataClass, string>>>
        {
            a => a.PropertyOne,
            b => b.Child.PropertyThree
        };
    }
}

Тогда сигнатура рассматриваемого метода может быть такой:

public static Expression<Func<T, bool>> Match<T>(
    this IEnumerable<Expression<Func<T, string>>> fields, string text, bool ignoreCase)

с использованием, подобным этому

var dataList = new List<DataClass> { data };
var query = dataList.AsQueryable()
    .Where(DataClass.GetFields().Match("lorem", true));

Теперь реализация.Желаемое выражение может быть построено исключительно с помощью методов класса Expression, но я покажу вам более простой (IMHO) метод, который формирует выражение из выражения времени компиляции путем замены параметра (ов) другими выражениями.

Все, что вам нужно, это небольшой вспомогательный метод для замены параметра лямбда-выражения другим выражением:

public static partial class ExpressionUtils
{
    public static Expression ReplaceParameter(this Expression expression, ParameterExpression source, Expression target)
    {
        return new ParameterReplacer { Source = source, Target = target }.Visit(expression);
    }

    class ParameterReplacer : ExpressionVisitor
    {
        public ParameterExpression Source;
        public Expression Target;
        protected override Expression VisitParameter(ParameterExpression node)
            => node == Source ? Target : base.VisitParameter(node);
    }
}

Внутренне он использует ExpressionVistor, чтобы найти каждый экземпляр переданного ParameterExpression и заменить егос переданным Expression.

С помощью этого вспомогательного метода реализация может быть такой:

public static partial class ExpressionUtils
{
    public static Expression<Func<T, bool>> Match<T>(this IEnumerable<Expression<Func<T, string>>> fields, string text, bool ignoreCase)
    {
        Expression<Func<string, bool>> match;
        if (ignoreCase)
        {
            text = text.ToLower();
            match = input => input.ToLower().Contains(text);
        }
        else
        {
            match = input => input.Contains(text);
        }
        // T source =>
        var parameter = Expression.Parameter(typeof(T), "source");
        Expression anyMatch = null;
        foreach (var field in fields)
        {
            // a.PropertyOne --> source.PropertyOne
            // b.Child.PropertyThree --> source.Child.PropertyThree
            var fieldAccess = field.Body.ReplaceParameter(field.Parameters[0], parameter);
            // input --> source.PropertyOne
            // input --> source.Child.PropertyThree
            var fieldMatch = match.Body.ReplaceParameter(match.Parameters[0], fieldAccess);
            // matchA || matchB
            anyMatch = anyMatch == null ? fieldMatch : Expression.OrElse(anyMatch, fieldMatch);
        }
        if (anyMatch == null) anyMatch = Expression.Constant(false);
        return Expression.Lambda<Func<T, bool>>(anyMatch, parameter);
    }
}

input => input.ToLower().Contains(text) или input => input.Contains(text) - это наше выражение соответствия времени компиляции, котороезатем мы заменяем параметр input телом переданных лямбда-выражений Expression<Func<T, string>>, а их параметр заменяется общим параметром, используемым в конечном выражении.Полученные выражения bool объединяются с Expression.OrElse, который эквивалентен оператору C # || (тогда как Expression.Or - для побитового | оператора и, как правило, не должен использоваться с логическими операциями).То же самое для && - используйте Expression.AndAlso, а не Expression.And, что для побитового &.

Этот процесс в значительной степени эквивалентен выражению string.Replace.Если объяснений и комментариев к коду недостаточно, вы можете пройтись по коду и посмотреть точные преобразования выражений и процесс построения выражений.

0 голосов
/ 29 ноября 2018

В продолжение моего комментария выше, я думаю, вы могли бы сделать это так:

class DataClass 
{
    …

    static public Expression<Func<DataClass,bool>> MatchSelectedFields( string text, bool ignoreCase) 
    {
         return @this => (
             String.Equals( text, @this.PropertyOne, (ignoreCase? StringComparison.OrdinalIgnoreCase: StringComparison.Ordinal)) 
             || String.Equals( text, @this.Child.PropertyThree, (ignoreCase? StringComparison.OrdinalIgnoreCase: StringComparison.Ordinal))
         );
    }
}

Тогда запрос просто

     Expression<Func<DataClass,bool>> match = DataClass.MatchSelectedFields( "lorem", ignoreCase);
     IEnumerable<DataClass> results = dataList.Where( d => match(d));

Я бы не стал публиковать секундуОтвет, но я подумал, что было бы полезно увидеть, как избежать динамического изменения выражений.Предостережение: на самом деле я не пытался его скомпилировать.

0 голосов
/ 25 ноября 2018

Нет необходимости вдаваться в сложности динамического создания Expression, потому что вы можете просто вызвать делегат Func напрямую:

public interface IFieldExposer<T>
{
    IEnumerable<Func<T,string>> SelectedFields { get; }
}
public static class FieldExposerExtensions
{
    public static IEnumerable<Func<T,string>> MatchIgnoreCase<T>( this IEnumerable<Func<T,string>> stringProperties, T source, string matchText)
    {
        return stringProperties.Where(stringProperty => String.Equals( stringProperty( source), matchText, StringComparison.OrdinalIgnoreCase));
    }
}

class DataClass : IFieldExposer<DataClass>
{
    public string PropertyOne { get; set; }
    public string PropertyTwo { get; set; }
    public ChildClass Child { get; set; }

    public IEnumerable<Func<DataClass, string>> SelectedFields {
        get {
            return new Func<DataClass, string>[] { @this => @this.PropertyOne, @this => @this.Child.PropertyThree };
        }
    }

    public override string ToString() => this.PropertyOne + " " + this.PropertyTwo + " " + this.Child.PropertyThree;
}

class ChildClass
{
    public string PropertyThree { get; set; }
}

Затем, чтобы использовать его,

class Program
{
    static void Main(string[] args)
    {
        var data = new DataClass {
            PropertyOne = "Lorem",
            PropertyTwo = "Ipsum",
            Child = new ChildClass {
                PropertyThree = "Dolor"
            }
        };
        var data2 = new DataClass {
            PropertyOne = "lorem",
            PropertyTwo = "ipsum",
            Child = new ChildClass {
                PropertyThree = "doloreusement"
            }
        };

        var dataList = new List<DataClass>() { data, data2 };
        IEnumerable<DataClass> results = dataList.Where( d => d.SelectedFields.MatchIgnoreCase( d, "lorem").Any());
        foreach (DataClass source in results) {
            Console.WriteLine(source.ToString());
        }
        Console.ReadKey();
    }
}
...