tags:

views:

246

answers:

6

I'm writing what I believe should be a relatively straight-forward Windows Form app. I'm using LINQ to SQL, though I have never used it before. We have a SQL Server database, and I'm creating a front-end to access that database. I'm trying to figure out the most efficient way to search for multiple (arbitrary number of) search parameters with it.

In the windows form, I create a dictionary with each search key and its value to search for, and pass it into my search() method. I am trying to find a way to search the database with each of those keys and their associated values. Here is what I am trying to do:

public IQueryable<Product> Search(Dictionary<string, string> searchParams)
{
   DBDataContext dc = new DBDataContext();
   var query = dc.Products;

   foreach (KeyValuePair<string, string> temp in searchParams)
   {
      query = query.Where(x => x.(temp.Key) == temp.Value);
   }

   return query;
}

I realize that syntactically x.(temp.Key) is incorrect, but I hope that illustrates what I am trying to do. I was wondering if there is another way to go about what I am trying to do without having to do a giant switch statement (or if/else if tree).

EDIT

So, I revised it a little, but I'm still having issues with it. Here is what I currently have:

public IQueryable<Product> Search(Dictionary<string, string> searchParams)
{
    DBDataContext dc = new DBDataContext();

    string sQuery = "";
    foreach (KeyValuePair<string, string> temp in searchParams)
    {
        sQuery += temp.Key + "=" + temp.Value + " AND ";
    }

    var query = dc.Products.Where(sQuery);

    return query;
}

According to the LINQ Dynamic Query Library article, this should be OK. Here is the error I'm getting:

The type arguments for method 'System.Linq.Queryable.Where(System.Linq.IQueryable, System.Linq.Expressions.Expression>)' cannot be inferred from the usage. Try specifying the type arguments explicitly.

+1  A: 

If the Dictionary is not required for some reason, I would make your Search method as follows:

public IQueryable<Product> Search( Func<Product, bool> isMatch )
{
   DBDataContext dc = new DBDataContext();
   return dc.Products.Where( isMatch ).AsQueryable();
}

Then, you would use the method like so:

Obj.Search( item => item.Property1 == "Hello" && item.Property2 == "World" );

Is there some reason that you can't do that?

[Edit: added AsQueryable()]

[Edit: for Dynamic Query using strings]

Take a look here and see if this helps. I haven't used it, but looks like it's what you're looking for: http://weblogs.asp.net/scottgu/archive/2008/01/07/dynamic-linq-part-1-using-the-linq-dynamic-query-library.aspx

Personally, I would generally prefer the type-safe Expression> approach since this will give you compile time errors...but if strings are needed, then looks like the best way to do it.

Based on the above link, looks like you should be able to do:

query = query.Where( String.Format("{0}={1}",dict.Key,dict.Value) );

[Edit: String Building Example]

So, one of the problems is that your sql query is going to end with an AND at the end of the string but then no condition after it...so, might try changing to this...syntax might be off slightly, but should be right:

public IQueryable<Product> Search(Dictionary<string, string> searchParams)
{
    DBDataContext dc = new DBDataContext();

    StringBuilder sQuery = new StringBuilder();
    foreach (KeyValuePair<string, string> temp in searchParams)
    {
        if( sQuery.Length > 0 ) sQuery.Append(" AND ");
        sQuery.AppendFormat("{0}={1}",temp.Key,temp.Value);
    }

    var query = dc.Products.Where(sQuery.ToString());

    return query;
}

This will only use "AND" on conditions after the first. Hope it helps...

FYI - It's off-topic, but 'why' I used StringBuilder is that string concatenation the way you had it would result in the string being destroyed and a new string being created in memory 4 times per loop...so changed to a StringBuilder since that will create a buffer that can be filled and only resized when necessary.

Kevin Nelson
Your function will return `IEnumerable`, not `IQueryable`, so the code won't compile. Furthermore, rows from the database will be filtered in C# code, not in the database layer. It'll be slower and will require more memory.
Athari
Added AsQueryable() so that it should compile
Kevin Nelson
Sounds like Athari knows something about the Expression<> aspect, however, that I don't...so probably best to go with his answer.
Kevin Nelson
A: 

Instead of using dictionary, you can use this method:

using System.Linq.Expressions;
// ...

public static IQueryable<Product> Search(Expression<Func<Product, bool>> search)
{
    DBDataContext dc = new DBDataContext();
    return dc.Products.Where(search);
}

You can use it like this:

Search(p => p.Name == "Name" && p.Id == 0);

You also won't be limited to string properties.

Athari
So, what you're saying is that for Linq to SQL you need to use Expression in order for it to execute the query in the DB? Just doing Func<> will execute in C#?
Kevin Nelson
This still has the same inherent limitation that I am trying to avoid. I still need to explicitly type out the property p.Name, rather than having it by dynamically generated based on whatever keys are present in the Dictionary (p.[KEY] = [VALUE]). I am in no way tied or attached to using a Dictionary, but that just seemed to me to be the only method of storing keys to search for and their values.
jonathan
Okay, so the Dictionary was important...I added a link to my answer on using strings using MS Dynamic Query Library
Kevin Nelson
This is the error I'm getting with that: "Error 1 The type arguments for method 'System.Linq.Queryable.Where<TSource>(System.Linq.IQueryable<TSource>, System.Linq.Expressions.Expression<System.Func<TSource,bool>>)' cannot be inferred from the usage. Try specifying the type arguments explicitly."
jonathan
A: 

Here's an example that works (I just tested it), using the Dynamic LINQ Query Library.

using System.Linq.Dynamic;
// ...

Dictionary<string, string> searchParams = new Dictionary<string,string>();

searchParams.Add("EmployeeID", "78");
searchParams.Add("EmpType", "\"my emp type\"");

IQueryable<Employee> query = context.Employees;

foreach (KeyValuePair<string, string> keyValuePair in searchParams)
{
    query = query.Where(string.Format("{0} = {1}", keyValuePair.Key,  keyValuePair.Value));
}

List<Employee> employees = query.ToList();

And, to make it absolutely clear that this code actually works here is the actual generated SQL:

FROM [HumanResources].[Employee] AS [t0]
WHERE ([t0].[EmpType] = @p0) AND ([t0].[EmployeeID] = @p1)',N'@p0 nvarchar(11),@p1 int',@p0=N'my emp type',@p1=78
shaunmartin
Well it finally compiles, so that's a huge first step. THANK YOU so much to you and Kevin Nelson. Different problem now: When I enter in a value to search for, an exception is thrown in the DynamicLibrary that no property or field [value I searched for] exists in 'Products'. So it seems that rather than searching for the value in each entry's key, it is searching each entry for a field which is the value I want to search for. If I'm searching for a first name, it is looking for Employee.jonathan = first_name, rather than Employee.first_name = jonathan.
jonathan
I've tried switching the order of the query string to query = query.Where(string.Format("{0} = {1}", keyValuePair.Value, keyValuePair.Key)); and it still throws the same exception.
jonathan
In the debugger, it seems that no matter the order of the key and value in the dictionary, DynamicLibrary always searches against the value, rather than the key.
jonathan
Hmmm, it's hard to say without seeing your actual code. Switching key and value won't help. One strategy is to just temporarily hardcode your query string into the Where() to eliminate string building errors, and make sure it works that way first. Like: query = query.Where("EmployeeID = 78"); and so on.
shaunmartin
Also, you might want to use SQL Server Profiler to verify the generated SQL query. IIRC, the VS debugger isn't able to show you the SQL when you're using Dynamic.
shaunmartin
You're now open to all kinds of SQL injections. You don't escape values, you don't quote strings. The latter is probably the cause of exceptions. SELECT * FROM products WHERE name = tasty cake?
Athari
@Athari: Injections: **Dynamic LINQ processes the input string further to prevent injections.** See the third comment by Scott Gu himself on the Dynamic LINQ page I linked to in my answer. I have added the *actual* generated SQL to my answer. **I tested and verified the code in my answer (I always do) and it works as I have suggested.**
shaunmartin
@shaunmartin OK, sorry. Dynamic LINQ seems to be smarter than I thought. :) Still, it's like using a steam-hammer to crack nuts... I don't know why Jonathan insists on using pairs of strings to access values.
Athari
A: 

If the items key are Properties in dc.Products (it is if you using EF) you can use reflection to get properties value:

  item.Value.Equals(x.GetType().GetProperty(item.Key).GetValue(x,null))

So you have this:

foreach (KeyValuePair<string, string> temp in searchParams)
   {
      query = query.Where(x => item.Value.Equals(x.GetType().GetProperty(item.Key).GetValue(x,null)));
   }
SaeedAlg
I agree, reflection is awesome. Its how WinForms and WPF do databinding, so its mainstream (not a hack). I love it.
Jordan
Now you load the whole database table into memory just to use the slowest method of accessing values. It will probably work with 10 or 100 products. It will choke on 10,000 or 100,000 products.
Athari
@Athari, Where is a whole of DB? I'm not agree to do this, but its a thing that he wants. I prefer to use EF as a way EF suggests. not generic and general queries. Also this works for 10,000 item, I had a such situation and I should to create some dynamic queries so i do it like above, it works fine.
SaeedAlg
@Athari, if you left a down vote to me explain why?
SaeedAlg
@SaeedAlg I've already explained this, your method loads the whole table into memory and uses reflection, the slowest method available for reading properties, instead of using SQL for filtering results. Even if it works, it's slow and it's a bad practice. If somebody comes across this question, I don't want them to use your method. What Jonathan wants is a bad practice in the first place too, but you made it even worse. If Jonathan insists on using strings to read properties, this Dymamic LINQ thingy suggested by shaunmartin is probably the way to go.
Athari
@Athari, First, I can't understand where I'm going to load whole table and you or other do not doing this, Also if you worried about performance you can test it with timespan its performance is not as bad as shaunmartins way, doing what Jonathan wants is bad practice at all, but this is exactly what he wants, with smallest code. I never said that this is a good practice, question is false, and you know :`false=> x` is true at all. He got 6 point until now because of his bad way of thinking, may be some one learns how to fetch properties value. my answer is not as bad as Jonathan's question.
SaeedAlg