Question

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.

Was it helpful?

Solution

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

OTHER TIPS

You need to build full IQueryable query and just execute it at the end. Try so:

keywords.All(k=>{customers = customers.Where(x=>x.FirstName.Contains(k) || x.LastName.Contains(k) || x.CompanyName.Contains(k)); return true;});

var result = customers.ToList();

no you must use ToList to get your desired results because LINQ is totally collection base object terminology.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top