如何在使用LINQ to Entities和helper方法时保持DRY

本文关键字:方法 helper DRY Entities to LINQ | 更新日期: 2023-09-27 18:05:32

假设我有一种特殊的方式来决定某些字符串是否"匹配",就像这样:

public bool stringsMatch(string searchFor, string searchIn)
{
  if (string.IsNullOrEmpty(searchFor))
  {
    return true;
  }
  return searchIn != null &&
    (searchIn.Trim().ToLower().StartsWith(searchFor.Trim().ToLower()) ||
     searchIn.Contains(" " + searchFor));
}

我想拉出匹配数据库使用Linq到实体和这个助手。但是,当我尝试这样做时:

IQueryable<Blah> blahs = query.Where(b => stringsMatch(searchText, b.Name);

我得到"LINQ to Entities不识别方法…"

如果我将代码重写为:

IQueryable<Blah> blahs = query.Where(b =>
      string.IsNullOrEmpty(searchText) ||
      (b.Name != null &&
        (b.Name.Trim().ToLower().StartsWith(searchText.Trim().ToLower()) ||
         b.Name.Contains(" " + searchText)));

在逻辑上是等价的,那么一切正常。问题是代码可读性不高,我必须为我想要匹配的每个不同实体重写它。

从这样的问题中我可以看出,我想做的事情目前是不可能的,但我希望我错过了一些东西,是吗?

如何在使用LINQ to Entities和helper方法时保持DRY

如果要过滤的所有'blahs'(类)具有相同的结构,则可以使用这样的简单方法。主要区别在于它返回一个Linq应该能够解析的表达式,并且它带来了整个实例并对Name进行过滤,而不是只带来字符串名称。

    public static Expression<Func<T, bool>> BuildStringMatch<T>(string searchFor) where T : IHasName
    {
        return b =>
              string.IsNullOrEmpty(searchFor) ||
              (b.Name != null &&
                (b.Name.Trim().ToLower().StartsWith(searchFor.Trim().ToLower()) ||
                 b.Name.Contains(" " + searchFor)));
    }

你可以这样使用这个方法:

    IQueryable<Blah> blahs = query.Where(BuildStringMatch<Blah>(searchText));

假设您想要过滤的所有类都实现了一些接口,例如:

    public interface IHasName
    {
        string Name { get; }
    }

如果你想过滤不同的属性,我不认为这是你可以用这样简单的代码做到的。我相信你需要自己用反射来构建表达式(或者在使用反射的库的帮助下)——这仍然是可能的,但要困难得多。

编辑:听起来你需要动态行为,所以我从dtb对这个问题的回答中借用了一些逻辑,想出了这个:
public static Expression<Func<T, bool>> BuildStringMatch<T>(Expression<Func<T, string>> property, string searchFor)
{
    var searchForExpression = Expression.Constant(searchFor, typeof(string));
    return
        Expression.Lambda<Func<T, bool>>(
            Expression.OrElse(
                Expression.Call(typeof(string), "IsNullOrEmpty", null, searchForExpression),
                Expression.AndAlso(
                    Expression.NotEqual(property.Body, Expression.Constant(null, typeof(string))),
                    Expression.OrElse(
                        Expression.Call(Expression.Call(Expression.Call(property.Body, "Trim", null), "ToLower", null), "StartsWith", null,
                            Expression.Call(Expression.Call(searchForExpression, "Trim", null), "ToLower", null)),
                        Expression.Call(property.Body, "Contains", null, Expression.Call(typeof(string), "Concat", null, Expression.Constant(" "), searchForExpression))
                    )
                )
            ),
            property.Parameters
        );
}

你可以这样使用:

        IQueryable<Blah> blahs2 = query.Where(BuildStringMatch<Blah>(b => b.Name, searchText));

它很长很啰嗦,但你可以看到它与用c#代码编写的原始方法是如何相似的。注意:我没有测试这段代码,所以可能会有一些小问题——但这就是一般的想法。

使用免费提供的名为LINQKit的库(如@Eranga所提到的),此任务变得合理。使用LINQKit,我现在的代码看起来像:

protected Expression<Func<T, bool>> stringsMatch(string searchFor, Expression<Func<T, string>> searchIn)
{
  if (string.IsNullOrEmpty(searchFor))
  {
    return e => true;
  }
  return
    e =>
    (searchIn.Invoke(e) != null &&
      (searchIn.Invoke(e).Trim().ToLower().StartsWith(searchFor.Trim().ToLower()) ||
       searchIn.Invoke(e).Contains(" " + searchFor)));
}

并且需要像这样调用(注意AsExpandable()调用)

IQueryable<Blah> blahs = query().AsExpandable().Where(StringsMatch(searchText, b => b.Name));

神奇的部分是searchIn.Invoke(e)调用和AsExpandable()的使用,AsExpandable()添加了一个包装器层,允许它们工作。

AsExpandable()位由原作者在这里详细解释。

请注意,我对表达式的一些细节仍然有点模糊,所以如果它可以做得更好/更短/更清晰,请添加评论/编辑这个答案。