I'm trying to do a keyword search on an IQueryable
object, but is there any way to do this without first converting it into a list?
My code:
var customers = Customer.IQueryableAll(); // Method returns IQueryable containing all customers.
var customerHolder = new List<Customer>();
foreach(var k in keywords) //keywords = List<string>
{
customerHolder.AddRange(customers.Where(x=>x.FirstName.Contains(k) || x.LastName.Contains(k) || x.CompanyName.Contains(k)).ToList())
}
return customerHolder.Distinct();
This works if I want to display all the results at once, but the problem comes where I want to do paging. The function would still get all the entries from the database, before paging, so its very inefficient on large tables. (ie. customerHolder.Skip(5).Take(5);
)
Is there any way to integrate the foreach searching part into the query itself ?
ie.
customers.Where( x => x.Name.Contains(anythingInKeyWords));
EDIT: For further clarification, I do want to maintain the OR in the above, so filtering and refiltering with multiple where clauses will not work. IE. Bill Job/Bill Gates > Search Bill Gates should return both entries because Bill Matches.
If you use IQueryable with foreach then query will be evaluated and executed at the same time. Some of the important points about IQueryable<T> are as follows: It implements IEnumerable <T> so the results can be iterated using foreach. It is best suited to query against external data sources.
var list4 = list1. Union(list2); Union is a set operation - it returns distinct values. Concat simply returns the items from the first sequence followed by the items from the second sequence; the resulting sequence can include duplicate items.
The IQueryable interface inherits the IEnumerable interface so that if it represents a query, the results of that query can be enumerated. Enumeration causes the expression tree associated with an IQueryable object to be executed. The definition of "executing an expression tree" is specific to a query provider.
The simple answer is that unless you add the record to the underlying datastore that the Iqueryable is querying, you can't add a new record into an IQueryable. So if you are using LinqToSql then you would have to add a row into the table that the IQueryable was querying in order to "add" a row into the IQueryable.
You need to build a query that ORs the result of your filter expression for each keyword per entity, which isn't very practical without using dynamic LINQ. Here's an extension method that will do just that for you:
public static class ExtensionMethods
{
public static IQueryable<TEntity> TestPerKey<TEntity, TKey>(
this IQueryable<TEntity> query,
IEnumerable<TKey> keys,
Expression<Func<TEntity, TKey, bool>> testExpression )
{
// create expression parameter
var arg = Expression.Parameter( typeof( TEntity ), "entity" );
// expression body var
Expression expBody = null;
// for each key, invoke testExpression, logically OR results
foreach( var key in keys )
{
// constant expression for key
var keyExp = Expression.Constant( key );
// testExpression.Invoke expression
var invokeExp = Expression.Invoke( testExpression, arg, keyExp );
if( null == expBody )
{
// first expression
expBody = invokeExp;
}
else
{
// logically OR previous expression with new expression
expBody = Expression.OrElse( expBody, invokeExp );
}
}
// execute Where method w/ created filter expression
return query.Where( ( Expression<Func<TEntity, bool>> )Expression.Lambda( expBody, arg ) );
}
}
Usage:
class TestEntity
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string CompanyName { get; set; }
}
static void Main()
{
var testCollection = new TestEntity[]{
new TestEntity(){
Id = 0,
FirstName = "abc",
LastName = "def",
CompanyName = "ghi"
},
new TestEntity(){
Id = 1,
FirstName = "def",
LastName = "ghi",
CompanyName = "jkl"
},
new TestEntity(){
Id = 2,
FirstName = "ghi",
LastName = "jkl",
CompanyName = "mno"
},
new TestEntity(){
Id = 3,
FirstName = "bcd",
LastName = "efg",
CompanyName = "hij"
},
};
var keywords = new[]{
"abc",
"jkl"
};
var query = testCollection.AsQueryable()
.TestPerKey(
keywords,
( t, k ) =>
t.FirstName.Contains( k ) ||
t.LastName.Contains( k ) ||
t.CompanyName.Contains( k ) );
foreach( var result in query )
{
Console.WriteLine( result.Id );
}
}
Update - try the following extension method. It is more specific but should work with EF:
public static IQueryable<TestEntity> TestPerKey(
this IQueryable<TestEntity> query,
IEnumerable<string> keys )
{
MethodInfo containsMethodInfo = typeof( string ).GetMethod( "Contains" );
// create expression parameter
var arg = Expression.Parameter( typeof( TestEntity ), "entity" );
// expression body var
Expression expBody = null;
// for each key, invoke testExpression, logically OR results
foreach( var key in keys )
{
var expression = Expression.OrElse(
Expression.OrElse(
Expression.Call( Expression.Property( arg, "FirstName" ), containsMethodInfo, Expression.Constant( key ) ),
Expression.Call( Expression.Property( arg, "LastName" ), containsMethodInfo, Expression.Constant( key ) ) )
, Expression.Call( Expression.Property( arg, "CompanyName" ), containsMethodInfo, Expression.Constant( key ) ) );
if( null == expBody )
{
// first expression
expBody = expression;
}
else
{
// logically OR previous expression with new expression
expBody = Expression.OrElse( expBody, expression );
}
}
// execute Where method w/ created filter expression
return query.Where( ( Expression<Func<TestEntity, bool>> )Expression.Lambda( expBody, arg ) );
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With