LINQ string []针对多个字段

时间:2011-08-17 02:59:43

标签: c# linq

假设我有一个包含以下字段的表dataContext.Customer

    FName    varchar
    LName    varchar
    Phone    varchar
    DOB      datetime
    Address  varchar

表中填充了一些示例数据,让我们说:

    John | Smith | 3051112222 | 01/01/1978 | Roosevelt Av 787
    Aron | Frank | 7871112222 | 01/01/1979 | Lambda Street 305
    Dick | Bush  | 9512221111 | 01/01/1980 | John Street 1
    John | Allen | 7872222222 | 01/01/1981 | Liberty Av 555

我们还有一个包含任意数量元素的字符串数组,例如:

    search[0] = "1978"
    search[1] = "John"

我需要一个LINQ查询,它将使用“contains”或“any”(在SQL中表示LIKE)逐步比较表的每个字段与字符串数组中的每个项目,并仅返回匹配所有给定条件的行记录,基于先前的search []示例,LINQ查询应仅返回记录#1。

另一个例子可以是:

    search[0] = "Bush"
    search[1] = "111"
    search[2] = "John"

只返回记录#3。最后:

    search[0] = "John"

应该返回记录#1,#3和#4(我认为这个想法很清楚)

关于如何将字符串[]与字段进行比较的问题: LINQ: Entity string field contains any of an array of strings

如果答案是50行C#例程,我更喜欢通过存储过程直接在数据库中解决这个问题。

如果有一些“反射”技巧可以在执行查询时迭代dataContext.Customers上的所有字段(显然真实表没有5个字段),那将是非常棒的。

性能不是问题。

我很确定这不可能在单个LINQ行中完成,因为多重匹配需要逻辑,但是问题永远不会受到伤害,更不用说学习任何新东西了。)

更新: 好的,这是一个简单的SQL代码,可以完成任务。请注意,为清晰起见,我已将搜索变量的数量减少到2。在现实生活中,我们可以将参数数量限制为10个搜索参数。我故意不使用函数(好吧,除了CONVERT)来保持SQL尽可能简单,看看是否有任何方法可以在LINQ中实现这一点。这是SQL:

    declare @_SEARCH1 varchar(1000)
    select  @_SEARCH1 = 'John'
    declare @_SEARCH2 varchar(1000)
    select  @_SEARCH2 = '111'

    select  *
    from    CUSTOMER
    where
            FName + ' ' + LName + ' ' + Phone + ' ' + CONVERT(varchar, DOB, 101) + ' ' + Address like '%'+@_SEARCH1+'%'
    and     FName + ' ' + LName + ' ' + Phone + ' ' + CONVERT(varchar, DOB, 101) + ' ' + Address like '%'+@_SEARCH2+'%'

所以问题是,有没有办法编写一个能生成这个简单SQL的LINQ? (请注意,比较是通过“LIKE”在数据库中完成的,而不是在应用程序中完成)

更新2:尽管像Francisco那样的解决方案会生成“LIKE”语句,但它将无法进行比较。将表格中的所有数据提取到网络服务器的其他解决方案将正确匹配,但是完全不切实际。

RUNE FS接受的答案,因为它是最干净的解决方案,可以在任何领域工作。

6 个答案:

答案 0 :(得分:5)

使用PredicateBuilder

void Main()
{
    var search = new string[] { "Romania","RO"};
    var query = from c in countries.AllAny(search)
        orderby c.name
        select c;
    query.Dump();
}

public static class QueryExtensions
{
    public static IQueryable<T> AllAny<T>(this IQueryable<T> query, string[] search)    
    {           
        var properties = typeof(T).GetProperties().Where(p => p.GetCustomAttributes(typeof(System.Data.Linq.Mapping.ColumnAttribute),true).Any()).Select(n=>n.Name);        
        var andPredicate = PredicateBuilder.True<T>();
        foreach ( var term in search )
        {
            var orPredicate = PredicateBuilder.False<T>();
            foreach (var property in properties )
                orPredicate = orPredicate.Or(CreateLike<T>(property,term));
            andPredicate = andPredicate.And(orPredicate);
        }
        return query.Where(andPredicate);
    }
    private static Expression<Func<T,bool>> CreateLike<T>( PropertyInfo prop, string value)
    {       
        var parameter = Expression.Parameter(typeof(T), "f");
        var propertyAccess = Expression.MakeMemberAccess(parameter, prop);            
        var toString = Expression.Call(propertyAccess, "ToString", null, null);
        var like = Expression.Call(toString, "Contains", null, Expression.Constant(value,typeof(string)));

        return Expression.Lambda<Func<T, bool>>(like, parameter);       
    }

    private static Expression<Func<T,bool>> CreateLike<T>( string propertyName, string value)
    {
        var prop = typeof(T).GetProperty(propertyName);     
        return CreateLike<T>(prop, value);
    }

}

// http://www.albahari.com/nutshell/predicatebuilder.aspx
public static class PredicateBuilder
{
  public static Expression<Func<T, bool>> True<T> ()  { return f => true;  }
  public static Expression<Func<T, bool>> False<T> () { return f => false; }

  public static Expression<Func<T, bool>> Or<T> (this Expression<Func<T, bool>> expr1,
                                                      Expression<Func<T, bool>> expr2)
  {
    var invokedExpr = Expression.Invoke (expr2, expr1.Parameters.Cast<Expression> ());
    return Expression.Lambda<Func<T, bool>>
          (Expression.OrElse (expr1.Body, invokedExpr), expr1.Parameters);
  }

  public static Expression<Func<T, bool>> And<T> (this Expression<Func<T, bool>> expr1,
                                                       Expression<Func<T, bool>> expr2)
  {
    var invokedExpr = Expression.Invoke (expr2, expr1.Parameters.Cast<Expression> ());
    return Expression.Lambda<Func<T, bool>>
          (Expression.AndAlso (expr1.Body, invokedExpr), expr1.Parameters);
  }
}

<强>更新 此代码是以下查询的通用解决方案

from c in countries
where (c.name.ToString().Contains(search[0]) || c.name.ToString().Contains(search[1]))
    && (c.iso_code.ToString().Contains(search[0]) || c.iso_code.ToString().Contains(search[1]))
    /*&& ...*/
orderby c.name
select c

此代码可以通过多种方式进行改进。对于示例,对于字符串属性,不需要在Contains之前调用ToString(这将生成convert(nvarchar)),我真的认为需要这个的人只想查看varchar,nvarchar列。

答案 1 :(得分:1)

我不认为linq to sql可以有效地做到这一点,但如果你可以在应用程序代码和数据库之间通过线路移动整个表,每次搜索只需要一次,linq到对象就可以提供帮助。

第一步是获取一个DataReader,它将读取表格中所有的记录。使用datareader很重要,因为你可能不想用整个表填充内存(然后再说一遍,也许你这样做 - 看看我关于缓存附近的缓存的说明)。

一旦有了这个,你需要将它转换为IEnumerable以与linq-to-objects一起使用。您可以使用简单的两行实用程序方法来实现,如下所示:

IEnumerable<IDataRecord> EnumerableFromDataReader(IDataReader reader)
{
    while (reader.Read())
        yield return reader;
}

我通常使用的实际代码稍微复杂一些,但是我所做的可能与你的项目其余部分的结构不太一致,所以我现在就把它放在这个简短的方法上。

一旦我们拥有了Enumerable,我们就可以利用linq的可组合特性来获得一些(相对)简单(或至少是简短的)代码:

IEnumerable<IDataRecord> SearchMyTable(string[] filters)
{
    var results = EnumerableFromDataReader(GetMyTable());

    foreach (string filter in filters)
    {
       results = results.Where(r => String.Join("",r.GetValues().Cast<string>().ToArray()).Contains(filter));
    }

    return results;
}

这不是一个单独的linq查询,但它也不是50行代码。

忽略网络延迟,此代码的性能实际上非常好。如果你想在列加入后对全部或部分表进行一些缓存,那么性能就是惊人的。

更新:此代码中存在(至少)一个缺陷。对于每个过滤器,我将存活到该过滤器的所有行重新转换为字符串...即使我已经为之前的过滤器执行了此操作。解决此问题的方法是首先进行投影以将行与字符串版本配对。但是因为它是在我的时间晚上11点之后,我将保留现在的代码。好消息是最终的固定代码应该是相同的长度:只需在第一行和最后一行添加.Select()调用,然后稍微更改foreach循环的中间位置。

答案 2 :(得分:1)

我现在手头没有C#编译器,但我有这个想法:

声明一个像这样的lambda表达式:

public Expression<Func<Customer, bool>> GetFilterFromString(string input)
{
    return p=> p.FName.Contains(input) ||
               p.LName.Contains(input) ||
               p.Phone.Contains(input) ||
               p.DOB.ToString().Contains(input) ||
               p.Address.Contains(input) ||    
}

实现可能因您的需求而异(例如连接所有字段,就像您对SQL查询所做的那样)。

然后在你的主要查询功能中:

IQueryable<Customer> customers = dataContext.Customers;
foreach(string inputSearch in search)
{
    customers = customers.Where(GetFilterFromString(inputSearch));
}
IEnumerable<Customer> results = customers.AsEnumerable();

我认为这种方法的主要优点是你必须声明一次GetFilterFromString。希望这是你正在寻找的。

修改

好的,所以我读了你正在寻找的SQL语句(有点迟到......但无论如何)。 我认为很容易适应我的解决方案。我们将不得不稍微调整lambda表达式:

public Expression<Func<Customer, bool>> GetFilterFromString(string input)
    {
        return p => (p.FName + " " +
                   p.LName + " " +                  
                   p.Phone + " " +
                   p.DOB.ToString() + " " +
                   p.Address)
                   .Contains(input) 
    }

答案 3 :(得分:1)

假设'\ t'永远不会成为数据的一部分,您可以执行以下操作。你当然可以替换任何其他角色。有了这个假设,你可以这样做:

public static IEnumerable<T> Where<T>(this IEnumerable<T> sequence, 
                                      string[] criteria){

var properties = typeof(T).GetProperties()
                          .Where(p=>p.GetGetMethod() != null);
return from s in sequence
       let text = properties.Aggregate("",(acc,prop) => 
                                               acc + 
                                               "\t" + 
                                               prop.GetValue(s,null)
                                       )
       where criteria.All(c => text.Contains(c))
       select s;

}

修改 我最初没有包含用法,因为我在原始帖子中没有找到任何集合,但假设序列被定义为IEnumerabl<Person>,并且可以作为变量db上的Persons属性进行访问。代码看起来类似于:

IEnumerable<Person> persons = db.Persons.Where(criteria);

答案 4 :(得分:0)

可能是我可能会做的是,首先生病从DB拥有名字的记录与我拥有的数组项目相比。

一旦我得到了表数据的子集(假设DB表的结构相同,数组结构也意味着数组[0]总是名字),那么我搜索我的模式在记忆中寻找。

我确信这不是您希望的完全解决方案。但是,lemme进一步思考,意味着当我对更多的想法开放时:)

答案 5 :(得分:0)

类似于Francisco的答案,但只使用了where子句:

string[] search = new string[] { "Bush", "111", "John" };
var customers = new[]   { 
                            new {FName = "Dick", Surname = "Bush", Phone = "9512221111", DOB = new DateTime(1980,01,01), Address = "John Street 1" },
                            new {FName = "John", Surname = "Smith", Phone = "3051112222",  DOB =  new DateTime(1978,01,01), Address = "Roosevelt Av 787"}
                        };


var result = customers.Where(customer => search.All(term =>
                    customer.FName.Contains(term)
                    || customer.Surname.Contains(term)
                    || customer.DOB.ToString().Contains(term)
                    || customer.Phone.Contains(term)
                    || customer.Address.Contains(term)
                    ));