针对多个字段的LINQ字符串[]

本文关键字:LINQ 字符串 字段 | 更新日期: 2023-09-27 18:06:03

假设我有一个表dataContext。具有以下字段的客户

    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)增量地比较表中的每个字段与字符串数组中的每个项目,并且只返回与记录中所有给定条件匹配的行,基于前面的搜索[]示例,LINQ查询应该只返回记录#1。

另一个例子是:

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

并且只返回记录#3。最后:

    search[0] = "John"

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

有一个关于如何比较字符串[]和字段的问题:LINQ:实体字符串字段包含任何字符串数组

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

如果有某种"反射"技巧来迭代dataContext上的所有字段,那将是非常棒的。客户在执行查询时(显然真实的表没有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+'%'

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

UPDATE 2:虽然像Francisco这样的解决方案将生成"like"语句,但它将无法进行比较。其他将所有数据从表中提取到web服务器的解决方案将正确匹配,但完全不切实际。

接受RUNE FS的答案,因为它是最干净的解决方案,并且可以在任何数量的字段中工作。

针对多个字段的LINQ字符串[]

使用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列。

假设''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);

我不认为linq to sql可以有效地做到这一点,但是如果您能够忍受每次搜索只在应用程序代码和数据库之间移动一次整个表,那么linq to objects可能会有所帮助。

第一步是获取一个DataReader,它将读取表中所有记录的。使用数据读取器是很重要的,因为您可能不希望用整个表填充内存(然后,也许您会这样做——参见我在末尾关于缓存的说明)。

一旦你有了它,你需要把它转换成一个IEnumerable来使用链接到对象。您可以使用一个简单的两行实用程序方法来完成,如下所示:

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行代码。

忽略网络延迟,这段代码的性能实际上是相当不错的。如果您想在列连接后对表的全部或部分进行缓存,那么性能将是惊人的。

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

我现在手头没有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) 
    }

我可能要做的是,首先我将从DB中获得与我所拥有的数组项比较的名字的记录。

一旦我得到表数据的子集(假设DB表的结构是相同的,数组结构也意味着数组[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)
                    ));