views:

112

answers:

3

Hi, I started with the IQueryable extension methods from this example on CodePlex.

What i believe i need is an IQueryable extension method to "Where", where the method signature looks like:

public static IQueryable<T> Where<T>(this IQueryable<T> source, string columnName, string keyword)

and effectively does this (assuming T.columnName is of type string):

source.Where(p => p.ColumnName.Contains("keyword"))

using the above CodePlex example, i think i understand how he got the OrderBy method working, but my problem seems a bit more complex and I don't know how to get the Contains("keyword") part working.

Thanks in advance,

--Ed

Update: 9/13/2010 6:26pm PST

I thought the following would work, but end up getting a NotSupportedException (The LINQ expression node type 'Invoke' is not supported in LINQ to Entities.) when I execute the expression via Count(). Any ideas?

    public static IQueryable<T> Where<T>(this IQueryable<T> source, string columnName, string keyword)
    {
        var type = typeof(T);
        var property = type.GetProperty(columnName);
        if (property.PropertyType == typeof(string))
        {
            var parameter = Expression.Parameter(type, "p");
            var propertyAccess = Expression.MakeMemberAccess(parameter, property);
            var sel = Expression.Lambda<Func<T, string>>(propertyAccess, parameter);
            var compiledSel = sel.Compile();
            return source.Where(item => compiledSel(item).Contains(keyword));
        }
        else
        {
            return source;
        }
    }
+1  A: 

The .Contains("keyword") part is exactly right in your example.

It's the p.ColumnName part that's going to cause trouble.

Now, there are a number of ways of doing this, generally involving either reflection or Expression<>, neither of which is particularly efficent.

The problem here is by passing the column name as a string, you are doing to undo that exact thing LINQ was invented to allow.

However, there are probably better ways of accomplishing your overall task besides that way.

So, let's look at alternate ways:

You want to be able to say :

   var selector = new Selector("Column1", "keyword");
   mylist.Where(item => selector(item));

and have it was the equivalent of

    mylist.Where(item=> item.Column1.Contains("keyword"));

How 'bout we go with:

   Func<MyClass, string> selector = i => i.Column1;
   mylist.Where(item => selector(item).Contains("keyword"));

or

   Func<MyClass, bool> selector = i => i.Column1.Contains("keyword");
   mylist.Where(item => selector(item));

These are easily expanded for alternatives:

   Func<MyClass, string> selector;
   if (option == 1)
        selector = i => i.Column1;
   else
        selector = i => i.Column2;
   mylist.Where(item => selector(item).Contains("keyword"));
James Curran
Thanks, i've incorporated your suggestions into my "work in progress".
Ed.S.
+1  A: 
public static IQueryable<T> Where<T>(
    this IQueryable<T> source, string columnName, string keyword)
{
    var arg = Expression.Parameter(typeof(T), "p");

    var body = Expression.Call(
        Expression.Property(arg, columnName),
        "Contains",
        null,
        Expression.Constant(keyword));

    var predicate = Expression.Lambda<Func<T, bool>>(body, arg);

    return source.Where(predicate);
}
dtb
this won't work because Queryable.Contains expects type T as a parameter, not string. This is why I need to do Queryable.Where(p => p.EdsColumn.Contains("searchthis")
Ed.S.
@Ed.S.: Changed `T` to `string`. Is that better?
dtb
I think there needs to be two bodies. One for the String.Contains and another for the Where. var stringBody = Expression.Call( typeof(string), "Contains", new Type[] { typeof(string) }, Expression.Property(arg, columnName), Expression.Constant(keyword)); var whereBody = Expression.Call( typeof(Queryable), "Where", new Type[] { typeof(Queryable) }, -- this is where i get lost.
Ed.S.
@Ed.S.: Your `whereBody` is exactly what `return source.Where(predicate);` in my code returns. It first builds the predicate `p => p.ColumnName.Contains("keyword")` and then returns `source.Where(predicate)`.
dtb
@dtb: still getting "No method 'Contains' exists on type 'System.String'." Any ideas? In the body I changed both "typeof" to be of string since Queryable.Contains<T>(this IQueryable<T>,...) wouldn't work
Ed.S.
@Ed.S.: Oh, you're talking about [String.Contains](http://msdn.microsoft.com/en-us/library/dy85x1sa.aspx) and not [Queryable.Contains](http://msdn.microsoft.com/en-us/library/bb338641.aspx). I've changed my answer accordingly.
dtb
@dtb: thanks for the help - works like a charm. I'm now going to try to get this to work for other column data types.
Ed.S.
A: 

See, in generics type of the object works dynamically. So when p.ColumnName is taken as string, the Contains of string is been executed.

Generally for any lambda expression you specify, it parses the thing into an Expression and ultimately produces the output at runtime.

If you see my post : http://www.abhisheksur.com/2010/09/use-of-expression-trees-in-lamda-c.html you will understand how it works actually.

abhishek