tags:

views:

154

answers:

4

I'm trying to figure out a way of querying an object in my datamodel and include only those parameters that are not null. Like below:

public List<Widget> GetWidgets(string cond1, string cond2, string cond3)
{
    MyDataContext db = new MyDataContext();
    List<Widget> widgets = (from w in db.Widgets
                            where 
                                ... if cond1 != null w.condition1 == cond1 ...
                                ... if cond2 != null w.condition2 == cond2 ...
                                ... if cond3 != null w.condition3 == cond3 ...
                            select w).ToList();
    return widgets;
}

Since the widgets table can get very large, I'd like to avoid doing this:

public List<Widget> GetWidgets(string cond1, string cond2, string cond3)
{
    MyDataContext db = new MyDataContext();
    List<Widget> widgets = db.Widgets.ToList();

    if(cond1 != null)
        widgets = widgets.Where(w => w.condition1 == cond1).ToList();

    if(cond2 != null)
        widgets = widgets.Where(w => w.condition2 == cond2).ToList();

    if(cond3 != null)
        widgets = widgets.Where(w => w.condition3 == cond3).ToList();

    return widgets;
}

I've looked at several example but don't really see anything that matches what I need to do.

+7  A: 

What you want to avoid is actually executing the query until you are ready:

public List<Widget> GetWidgets(string cond1, string cond2, string cond3)
{
    MyDataContext db = new MyDataContext();
    var widgets = db.Widgets;

    if(cond1 != null)
        widgets = widgets.Where(w => w.condition1 == cond1);

    if(cond2 != null)
        widgets = widgets.Where(w => w.condition2 == cond2);

    if(cond3 != null)
        widgets = widgets.Where(w => w.condition3 == cond3);

    return widgets.ToList();
}

Note how the ToList calls are removed. The query is not executed until you start iterating over it. Invoking ToList will force that to happen, so that the result can be put into a List<> and returned. I would even suggest to change the return value of the method to IEnumerable<Widget> and skipping the ToList call in the end:

public IEnumerable<Widget> GetWidgets(string cond1, string cond2, string cond3)
{
    MyDataContext db = new MyDataContext();
    var widgets = db.Widgets;

    if(cond1 != null)
        widgets = widgets.Where(w => w.condition1 == cond1);

   // [...]

    return widgets;
}

That way the calling code gets to decide when to execute the query (it may even add more conditions before doing so).

Fredrik Mörk
Is it up to the JIT compiler to get rid of the conditionals in the resulting query? Or does linq 'guarantee' some optimizations?
xtofl
@xtofl: not sure what you mean? What conditionals would you want to get rid of?
Fredrik Mörk
If the conditions are not met, they are not added to the expression tree.
Michael
@Fredrik: I was wrong. I forgot that the linq query is constructed before execution, and as Michael says: the 'optional' conditions are not in the query's expression tree.
xtofl
+1  A: 

How about something like this?

        IEnumerable<Widget> condQuery = (from w in db.Widgets);
        if(cond1 != null ) condQuery = condQuery.Where(w=> w.condition1 == cond1);
        if(cond2 != null ) condQuery = condQuery.Where(w=> w.condition2 == cond2);

etc...?

Vivek
Doesn’t compile.
Timwi
A: 

You're actually asking for a dispatcher within the linq query. The Where method takes a predicate, so you can build your predicate before creating the query.

-- EDIT -- at first, I thought it easier, wrote some pseudo code that didn't even compile. Now, howver, I think I got the point. This code will work; it separates building the where clause from applying it.

    static Predicate<Widget> combine( 
           Predicate<Widget> existing, 
           Predicate<Widget> condition )
    {
        var newpred = new Predicate<Widget>( w=> existing(w) && condition(w) );
        return newpred;

    }

and use this 'building' functionality like that:

    static void Main(string[] args)
    {
        string cond1 = "hi";
        string cond2 = "lo";
        string cond3 = null;
        var pr = new Predicate<Widget>( (Widget w ) => true );
        if (cond1 != null) pr = combine( pr, w => w.condition1 == cond1);
        if (cond2 != null) pr = combine( pr, w => w.condition2 == cond2);
        if (cond3 != null) pr = combine( pr, w => w.condition3 == cond3);

I tested it with a little helper array:

        var widgets = new Widget[]{
            new Widget (){ condition1 = "" },
            new Widget (){ condition1 = "hi", condition2 = "lo" }
        };

        var selected = widgets.Where( (w) => pr(w));

        foreach (var w in selected) {
            Console.WriteLine(w);
        }
xtofl
Doesn’t compile.
Timwi
Also, changes the logic. OP checks all non-null conditions, this only checks the first. OP also said that the conditions are strings, while this treats them as booleans.
curveship
Even if you did fix the compilation errors (strings are not implicitly convertible to bool), this version of the program has very different semantics than the original version. This version finds the first condition that can be tested and uses only it; the original version applied all possible conditions.
Eric Lippert
Your new version is better. I note that you've taken a slightly different tack than the other answers. Here you build up the *predicate* and then make a single Where clause, instead of making three predicates and three Where clauses. One minor improvement you could still make here is that the way you've written the query is bizarre. The combination of query syntax with "fluent" syntax looks weird, the trivial query is unnecessary and the predicate to Where is unnecessarily complicated.
Eric Lippert
I would have written either "var selected = from w in widgets where pr(w) select w;", or "var selected = widgets.Where(pr);"
Eric Lippert
@Eric Lippert: thanks - you can tell that I'm quite new to the linq syntax :)
xtofl
No worries! And note that you don't have to make a predicate out of the predicate. If you use Func<Widget, bool> instead of Predicate<Widget> then you can just say Where(pr) instead of the clumsy Where(w=>pr(w)), which adds no value.
Eric Lippert
@Eric Lippert: jee... I would have thought a `Predicate` _was_ a `Func`... Thanks.
xtofl
Indeed; if we had to go back and do it again, I suspect that the type system designers would have *structural* typing on delegate types. The idea that a Func<T, bool> and a Predicate<T> are different types that are not compatible seemed like a good idea at the time but has turned out to be rather painful in practice.
Eric Lippert
+2  A: 

Use an "or gate": preface every widget condition test with an "||" and a check to see if we're using that condition or not. If we're not, the second half of the "or" isn't evaluated. That's why it's a gate -- we don't go any further if the first part evaluates to true.

If I were writing it, I'd do it like below. I used the var syntatic sugar to hold LINQ query and moved the ToList() to the end.

public List<Widget> GetWidgets(string cond1, string cond2, string cond3) 
{ 
    MyDataContext db = new MyDataContext(); 
    var widgets = from w in db.Widgets 
                  where (cond1 == null || w.condition1 == cond1)
                     && (cond2 == null || w.condition2 == cond2)
                     && (cond3 == null || w.condition3 == cond3)
                  select w;
    return widgets.ToList();
} 

edit: grammar

curveship