如何创建一个通用方法来遍历对象的字段并将其用作 Where 谓词?

How to create a generic method to iterate through an object's fields and use it as a Where predicate?

我正在构建一个通用接口,以从 class 中公开选定的字符串属性,然后我想在每个字段中搜索文本,以检查它是否匹配。

这是我的 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'

我没有使用表达式树的经验。我花了几个小时阅读文档和其他类似问题的答案,但我仍然不明白如何实现我想要的...我不知道现在该做什么。

我正在控制台应用程序中对此进行测试,所以如果您想自己构建它,这是主要的 class:

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 的回答并将 IFieldExposer<T> 中的 GetFields() 方法更改为 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();

  [...]

}

然后我似乎必须用 Expression.Lambda 动态调用 selectedFieldsExp。我想到了:

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

这行得通,但我不知道如何为 lambda 表达式正确调用 DynamicInvoke()

如果我不带参数调用它,它会抛出 Parameter count mismatch.,如果我调用它,它会抛出 Object of type 'System.Linq.Expressions.TypedParameterExpression' cannot be converted to type 'DataClass'. DynamicInvoke(parameter).

有什么想法吗?

无需深入了解动态创建表达式的复杂性,因为您可以直接调用 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();
    }
}

在开始实施之前,有一些设计缺陷需要修复。

首先,几乎所有查询提供程序(LINQ to Object 除外,它只是将 lambda 表达式编译为委托并执行它们)不支持调用表达式和自定义(未知)方法。那是因为它们不执行表达式,而是将它们翻译成其他东西(例如SQL),并且翻译是基于先验知识。

调用表达式的一个示例是 Func<...> 委托。所以你应该做的第一件事就是在你当前有 Func<...>.

的地方使用 Expression<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 class 方法构建,但我将向您展示一种更简单的(恕我直言)方法,该方法通过将参数替换为其他表达式来从编译时表达式中组成表达式(s).

您只需要一个小的辅助工具方法,用另一个表达式替换 lambda 表达式参数:

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) 是我们的编译时匹配表达式,然后我们用传递的 Expression<Func<T, string>> lambda 表达式的主体替换 input 参数,用它们的参数替换为最终表达式中使用的通用参数。生成的 bool 表达式与 Expression.OrElse 组合,它等效于 C# || 运算符(而 Expression.Or 用于按位 | 运算符,通常不应与逻辑运算符一起使用操作)。顺便说一句,&& - 使用 Expression.AndAlso 而不是 Expression.And,后者用于按位 &.

这个过程几乎相当于 string.Replace 的表达式。如果解释和代码注释不够,您可以逐步查看代码并查看确切的表达式转换和表达式构建过程。

根据我上面的评论,我认为你可以这样做:

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

我通常不会 post 第二个答案,但我认为了解如何避免动态修改表达式会很有用。 警告:我实际上并没有尝试编译它。