Compose LINQ-to-SQL predicates into a single predicate - c#

(An earlier question, Recursively (?) compose LINQ predicates into a single predicate, is similar to this but I actually asked the wrong question... the solution there satisfied the question as posed, but isn't actually what I need. They are different, though. Honest.)
Given the following search text:
"keyword1 keyword2 ... keywordN"
I want to end up with the following SQL:
SELECT [columns] FROM Customer
WHERE (
Customer.Forenames LIKE '%keyword1%'
OR
Customer.Forenames LIKE '%keyword2%'
OR
...
OR
Customer.Forenames LIKE '%keywordN%'
) AND (
Customer.Surname LIKE '%keyword1%'
OR
Customer.Surname LIKE '%keyword2%'
OR
....
OR
Customer.Surname LIKE '%keywordN%'
)
Effectively, we're splitting the search text on spaces, trimming each token, constructing a multi-part OR clause based on each , and then AND'ing the clauses together.
I'm doing this in Linq-to-SQL, and I have no idea how to dynamically compose a predicate based on an arbitrarily-long list of subpredicates. For a known number of clauses, it's easy to compose the predicates manually:
dataContext.Customers.Where(
(
Customer.Forenames.Contains("keyword1")
||
Customer.Forenames.Contains("keyword2")
) && (
Customer.Surname.Contains("keyword1")
||
Customer.Surname.Contains("keyword2")
)
);
In short, I need a technique that, given two predicates, will return a single predicate composing the two source predicates with a supplied operator, but restricted to the operators explicitly supported by Linq-to-SQL. Any ideas?

You can use the PredicateBuilder class
IQueryable<Customer> SearchCustomers (params string[] keywords)
{
var predicate = PredicateBuilder.False<Customer>();
foreach (string keyword in keywords)
{
// Note that you *must* declare a variable inside the loop
// otherwise all your lambdas end up referencing whatever
// the value of "keyword" is when they're finally executed.
string temp = keyword;
predicate = predicate.Or (p => p.Forenames.Contains (temp));
}
return dataContext.Customers.Where (predicate);
}
(that's actually the example from the PredicateBuilder page, I just adapted it to your case...)
EDIT:
Actually I misread your question, and my example above only covers a part of the solution... The following method should do what you want :
IQueryable<Customer> SearchCustomers (string[] forenameKeyWords, string[] surnameKeywords)
{
var predicate = PredicateBuilder.True<Customer>();
var forenamePredicate = PredicateBuilder.False<Customer>();
foreach (string keyword in forenameKeyWords)
{
string temp = keyword;
forenamePredicate = forenamePredicate.Or (p => p.Forenames.Contains (temp));
}
predicate = PredicateBuilder.And(forenamePredicate);
var surnamePredicate = PredicateBuilder.False<Customer>();
foreach (string keyword in surnameKeyWords)
{
string temp = keyword;
surnamePredicate = surnamePredicate.Or (p => p.Surnames.Contains (temp));
}
predicate = PredicateBuilder.And(surnamePredicate);
return dataContext.Customers.Where(predicate);
}
You can use it like that:
var query = SearchCustomers(
new[] { "keyword1", "keyword2" },
new[] { "keyword3", "keyword4" });
foreach (var Customer in query)
{
...
}

Normally you would chain invocations of .Where(...). E.g.:
var a = dataContext.Customers;
if (kwd1 != null)
a = a.Where(t => t.Customer.Forenames.Contains(kwd1));
if (kwd2 != null)
a = a.Where(t => t.Customer.Forenames.Contains(kwd2));
// ...
return a;
LINQ-to-SQL would weld it all back together into a single WHERE clause.
This doesn't work with OR, however. You could use unions and intersections, but I'm not sure whether LINQ-to-SQL (or SQL Server) is clever enough to fold it back to a single WHERE clause. OTOH, it won't matter if performance doesn't suffer. Anyway, it would look something like this:
<The type of dataContext.Customers> ff = null, ss = null;
foreach (k in keywords) {
if (keywords != null) {
var f = dataContext.Customers.Where(t => t.Customer.Forenames.Contains(k));
ff = ff == null ? f : ff.Union(f);
var s = dataContext.Customers.Where(t => t.Customer.Surname.Contains(k));
ss = ss == null ? s : ss.Union(s);
}
}
return ff.Intersect(ss);

Related

LINQ to SQL - Select where starts with any of list

Working on a Linq-to-SQL project and observing some odd behavior with the generated SQL. Basically I have an array of strings, and I need to select all rows where a column starts with one of those strings.
using (SqlConnection sqlConn = new SqlConnection(connString))
{
using (IdsSqlDataContext context = new IdsSqlDataContext(sqlConn))
{
//generated results should start with one of these.
//in real code base they are obviously not hardcoded and list is variable length
string[] args = new string[] { "abc", "def", "hig" };
IQueryable<string> queryable = null;
//loop through the array, the first time through create an iqueryable<>, and subsequent passes union results onto original
foreach (string arg in args)
{
if (queryable == null)
{
queryable = context.IdsForms.Where(f => f.MatterNumber.StartsWith(arg)).Select(f => f.MatterNumber);
}
else
{
queryable = queryable.Union(context.IdsForms.Where(f => f.MatterNumber.StartsWith(arg)).Select(f => f.MatterNumber));
}
}
//actually execute the query.
var result = queryable.ToArray();
}
}
I would expect the sql generated to be functionally equivalent to the following.
select MatterNumber
from IdsForm
where MatterNumber like 'abc%' or MatterNumber like 'def%' or MatterNumber like 'hig%'
But the actual SQL generated is below, notice 'hig%' is the argument for all three like clauses.
exec sp_executesql N'SELECT [t4].[MatterNumber]
FROM (
SELECT [t2].[MatterNumber]
FROM (
SELECT [t0].[MatterNumber]
FROM [dbo].[IdsForm] AS [t0]
WHERE [t0].[MatterNumber] LIKE #p0
UNION
SELECT [t1].[MatterNumber]
FROM [dbo].[IdsForm] AS [t1]
WHERE [t1].[MatterNumber] LIKE #p1
) AS [t2]
UNION
SELECT [t3].[MatterNumber]
FROM [dbo].[IdsForm] AS [t3]
WHERE [t3].[MatterNumber] LIKE #p2
) AS [t4]',N'#p0 varchar(4),#p1 varchar(4),#p2 varchar(4)',#p0='hig%',#p1='hig%',#p2='hig%'
Looks like you're closing over the loop variable. This is a common gotcha in C#. What happens is that the value of arg is evaluated when the query is run, not when it is created.
Create a temp variable to hold the value:
foreach (string arg in args)
{
var temp = arg;
if (queryable == null)
{
queryable = context.IdsForms.Where(f => f.MatterNumber.StartsWith(temp)).Select(f => f.MatterNumber);
}
else
{
queryable = queryable.Union(context.IdsForms.Where(f => f.MatterNumber.StartsWith(temp)).Select(f => f.MatterNumber));
}
}
You can read this Eric Lippert post about closing over a loop variable. As Eric notes at the top of the article, and as #Magus points out in a comment, this has changed in C# 5 so that the foreach variable is a new copy on each iteration. Creating a temp variable, like above, is forward compatible though.
The union is correct, due to you using union in your linq to sql query. The reason they are all hig% is because the lambda f => f.MatterNumber.StartsWith(arg) creates a closure around the loop parameter. To fix, declare a local variable in the loop
foreach (string arg in args)
{
var _arg = arg;
if (queryable == null)
{
queryable = context.IdsForms.Where(f => f.MatterNumber.StartsWith(_arg)).Select(f => f.MatterNumber);
}
else
{
queryable = queryable.Union(context.IdsForms.Where(f => f.MatterNumber.StartsWith(_arg)).Select(f => f.MatterNumber));
}
}
But I agree the union seems unnecessary. If the array of strings to check against is not going to change, then you can just use a standard where clause. Otherwise you could take a look at predicate builder! Check here
How about this ?
queryable = context..IdsForms.Where(f =>
{
foreach (var arg in args)
{
if (f.MatterNumber.StartsWith(arg))
return true;
}
return false;
}).Select(f => f.MatterNumber);

dynamically append multiple linq expressions at run-time

I have two similar methods that take a criteria object (dumb object with a list of properties), call a "CreateExpression" method on that criteria object, and then use the returned expression to filter results.
One of my examples has an argument of only one criteria, and it works with no problem. My second method takes a List<Criteria> and then attempts to iterate through each criteria in the list, and generate the expression for it, and then "and" it to the previous expression. The end result is supposed to be one big expression that I can then use in my linq query.
However, this second method is not working. When I use the debugger, I can see the predicate with it's body and lambda expression internally, but when it hits the SQL server, all it sends is a select statement with no where clauses at all.
Here is the method that works (with one criteria object):
public static List<Segment> GetByCriteria(Criteria.SegmentCriteria myCriteria)
{
List<Segment> result = null;
List<Segment> qry = db.Segments.AsExpandable<Segment>().Where<Segment>(CreateCriteriaExpression(myCriteria)).ToList<Segment>();
qry = qry.Where<Segment>(CreateCriteriaExpressionForCustomProperties(myCriteria).Compile()).ToList<Segment>();
if (qry != null && qry.Count != 0)
{
result = qry;
}
return result;
}
Here is the one that doesn't work:
public static List<Segment> GetByCriteria(List<Criteria.SegmentCriteria> myCriteria, Common.MultipleCriteriaMatchMethod myMatchMethod)
{
List<Segment> result = null;
var predicate = PredicateBuilder.True<Segment>();
var customPropertiesPredicate = PredicateBuilder.True<Segment>();
foreach (Criteria.SegmentCriteria x in myCriteria)
{
if (myMatchMethod == Common.MultipleCriteriaMatchMethod.MatchOnAll)
{
predicate = predicate.And(CreateCriteriaExpression(x).Expand());
customPropertiesPredicate = customPropertiesPredicate.And(CreateCriteriaExpressionForCustomProperties(x).Expand());
}
else if (myMatchMethod == Common.MultipleCriteriaMatchMethod.MatchOnAny)
{
predicate = predicate.Or(CreateCriteriaExpression(x).Expand());
customPropertiesPredicate = customPropertiesPredicate.Or(CreateCriteriaExpressionForCustomProperties(x).Expand());
}
}
List<Segment> qry = db.Segments.AsExpandable<Segment>().Where<Segment>(predicate.Expand()).ToList<Segment>();
qry = qry.Where<Segment>(customPropertiesPredicate.Expand().Compile()).ToList<Segment>();
if (qry != null && qry.Count != 0)
{
result = qry;
}
return result;
}
I am using Predicate Builder to generate the initial expression. I don't believe there is a problem with those methods since they work with the first (singular) method.
Does anyone know what's going on here?
Edit
I forgot to say that the backend is entity framework.
Ok, I figured it out.
I needed to use the "Expand()" method on ALL places where I was calling a predicate in the lines where I was appending the predicate. Here is the new fixed version of the foreach loop in my second method:
foreach (Criteria.SegmentCriteria x in myCriteria)
{
Criteria.SegmentCriteria item = x;
if (myMatchMethod == Common.MultipleCriteriaMatchMethod.MatchOnAll)
{
predicate = predicate.Expand().And<Segment>(CreateCriteriaExpression(item).Expand());
customPropertiesPredicate = customPropertiesPredicate.Expand().And<Segment>(CreateCriteriaExpressionForCustomProperties(item).Expand());
}
else if (myMatchMethod == Common.MultipleCriteriaMatchMethod.MatchOnAny)
{
predicate = predicate.Expand().Or<Segment>(CreateCriteriaExpression(item).Expand());
customPropertiesPredicate = customPropertiesPredicate.Expand().Or<Segment>(CreateCriteriaExpressionForCustomProperties(item).Expand());
}
}
And now it works! I found details about this also on this other question.

Multiple search parameters with LINQ

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.
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
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.
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.
have u try put single quote for your string?
based on your foreach loop, there will be extra 'AND' at the end of query
try this instead
string sQuery = searchParam.Select(entry => string.Format("{0} = '{1}'", entry.Key, entry.Value)).Aggregate((current, next) => current + " AND " next);
if you are willing to create a dictionary of searchable terms than I've used something close to the approach below. I've adapted what I do be more closely fit into how you were doing things, I've also changed the names of the tables to fit a datacontext / object model that I have available to me.
The idea is to create a keyed list of terms that your query supports searching by. Then add functions that return an expression which can then be passed into the where clause of your query.
You should of course add some error handling for invalid keys etc.
public IQueryable<Person> Search(Dictionary<string, string> searchParams)
{
DBDataContext dc = new DBDataContext();
var query = dc.Persons.Where(p => true); //do an 'empty predicate' because you want 'query' to be an iqueryable<Person> not a Table<Person>
//build a list of the types of things you can filter on.
var criteriaDefinitions = new Dictionary<string,Func<string,Expression<Func<Person,bool>>>>();
criteriaDefinitions.Add("FirstName",s => p => p.FirstName == s);
criteriaDefinitions.Add("LastName",s => p => p.LastName == s);
//you can do operations other than just equals
criteriaDefinitions.Add("EmailContains",s => p => p.Email.Contains(s));
//you can even create expressions that integrate joins.
criteriaDefinitions.Add("HasContactInCity",s => p => p.Contacts.Any(c => c.City == s));
foreach (KeyValuePair<string, string> temp in searchParams)
{
//grab the correct function out of the dictionary
var func = criteriaDefinitions[temp.Key];
//evaluating the function will return an expression which can passed into the where clause.
var expr = func(temp.Value);
query = query.Where(expr);
}
return query;
}
for linq search queries multiple optional parameters
Can Use This Code:
var query = dx.GetAllJobs().Where(x => x.JobName.Contains(keyword));
if (SecLink != 0)
{
query = query.Where(x => x.SectorLink.Equals(SecLink));
}
if (LocLink != 0)
{
query = query.Where(x => x.LocationLink.Equals(LocLink));
}
if (IndLink != 0)
{
query = query.Where(x => x.IndustryLink.Equals(IndLink));
}
if (VacLink != 0)
{
query = query.Where(x => x.VacancyTypeLink.Equals(VacLink));
}
var lstJobs = query.ToList();

c# finding matching words in table column using Linq2Sql

I am trying to use Linq2Sql to return all rows that contain values from a list of strings. The linq2sql class object has a string property that contains words separated by spaces.
public class MyObject
{
public string MyProperty { get; set; }
}
Example MyProperty values are:
MyObject1.MyProperty = "text1 text2 text3 text4"
MyObject2.MyProperty = "text2"
For example, using a string collection, I pass the below list
var list = new List<>() { "text2", "text4" }
This would return both items in my example above as they both contain "text2" value.
I attempted the following using the below code however, because of my extension method the Linq2Sql cannot be evaluated.
public static IQueryable<MyObject> WithProperty(this IQueryable<MyProperty> qry,
IList<string> p)
{
return from t in qry
where t.MyProperty.Contains(p, ' ')
select t;
}
I also wrote an extension method
public static bool Contains(this string str, IList<string> list, char seperator)
{
if (str == null) return false;
if (list == null) return true;
var splitStr = str.Split(new char[] { seperator },
StringSplitOptions.RemoveEmptyEntries);
bool retval = false;
int matches = 0;
foreach (string s in splitStr)
{
foreach (string l in list)
{
if (String.Compare(s, l, true) == 0)
{
retval = true;
matches++;
}
}
}
return retval && (splitStr.Length > 0) && (list.Count == matches);
}
Any help or ideas on how I could achieve this?
Youre on the right track. The first parameter of your extension method WithProperty has to be of the type IQueryable<MyObject>, not IQueryable<MyProperty>.
Anyways you dont need an extension method for the IQueryable. Just use your Contains method in a lambda for filtering. This should work:
List<string> searchStrs = new List<string>() { "text2", "text4" }
IEnumerable<MyObject> myFilteredObjects = dataContext.MyObjects
.Where(myObj => myObj.MyProperty.Contains(searchStrs, ' '));
Update:
The above code snippet does not work. This is because the Contains method can not be converted into a SQL statement. I thought a while about the problem, and came to a solution by thinking about 'how would I do that in SQL?': You could do it by querying for each single keyword, and unioning all results together. Sadly the deferred execution of Linq-to-SQL prevents from doing that all in one query. So I came up with this compromise of a compromise. It queries for every single keyword. That can be one of the following:
equal to the string
in between two seperators
at the start of the string and followed by a seperator
or at the end of the string and headed by a seperator
This spans a valid expression tree and is translatable into SQL via Linq-to-SQL. After the query I dont defer the execution by immediatelly fetch the data and store it in a list. All lists are unioned afterwards.
public static IEnumerable<MyObject> ContainsOneOfTheseKeywords(
this IQueryable<MyObject> qry, List<string> keywords, char sep)
{
List<List<MyObject>> parts = new List<List<MyObject>>();
foreach (string keyw in keywords)
parts.Add((
from obj in qry
where obj.MyProperty == keyw ||
obj.MyProperty.IndexOf(sep + keyw + sep) != -1 ||
obj.MyProperty.IndexOf(keyw + sep) >= 0 ||
obj.MyProperty.IndexOf(sep + keyw) ==
obj.MyProperty.Length - keyw.Length - 1
select obj).ToList());
IEnumerable<MyObject> union = null;
bool first = true;
foreach (List<MyObject> part in parts)
{
if (first)
{
union = part;
first = false;
}
else
union = union.Union(part);
}
return union.ToList();
}
And use it:
List<string> searchStrs = new List<string>() { "text2", "text4" };
IEnumerable<MyObject> myFilteredObjects = dataContext.MyObjects
.ContainsOneOfTheseKeywords(searchStrs, ' ');
That solution is really everything else than elegant. For 10 keywords, I have to query the db 10 times and every time catch the data and store it in memory. This is wasting memory and has a bad performance. I just wanted to demonstrate that it is possible in Linq (maybe it can be optimized here or there, but I think it wont get perfect).
I would strongly recommend to swap the logic of that function into a stored procedure of your database server. One single query, optimized by the database server, and no waste of memory.
Another alternative would be to rethink your database design. If you want to query contents of one field (you are treating this field like an array of keywords, seperated by spaces), you may simply have chosen an inappropriate database design. You would rather want to create a new table with a foreign key to your table. The new table has then exactly one keyword. The queries would be much simpler, faster and more understandable.
I haven't tried, but if I remember correctly, this should work:
from t in ctx.Table
where list.Any(x => t.MyProperty.Contains(x))
select t
you can replace Any() with All() if you want all strings in list to match
EDIT:
To clarify what I was trying to do with this, here is a similar query written without linq, to explain the use of All and Any
where list.Any(x => t.MyProperty.Contains(x))
Translates to:
where t.MyProperty.Contains(list[0]) || t.MyProperty.Contains(list[1]) ||
t.MyProperty.Contains(list[n])
And
where list.Any(x => t.MyProperty.Contains(x))
Translates to:
where t.MyProperty.Contains(list[0]) && t.MyProperty.Contains(list[1]) &&
t.MyProperty.Contains(list[n])

How can I conditionally apply a Linq operator?

We're working on a Log Viewer. The use will have the option to filter by user, severity, etc. In the Sql days I'd add to the query string, but I want to do it with Linq. How can I conditionally add where-clauses?
if you want to only filter if certain criteria is passed, do something like this
var logs = from log in context.Logs
select log;
if (filterBySeverity)
logs = logs.Where(p => p.Severity == severity);
if (filterByUser)
logs = logs.Where(p => p.User == user);
Doing so this way will allow your Expression tree to be exactly what you want. That way the SQL created will be exactly what you need and nothing less.
If you need to filter base on a List / Array use the following:
public List<Data> GetData(List<string> Numbers, List<string> Letters)
{
if (Numbers == null)
Numbers = new List<string>();
if (Letters == null)
Letters = new List<string>();
var q = from d in database.table
where (Numbers.Count == 0 || Numbers.Contains(d.Number))
where (Letters.Count == 0 || Letters.Contains(d.Letter))
select new Data
{
Number = d.Number,
Letter = d.Letter,
};
return q.ToList();
}
I ended using an answer similar to Daren's, but with an IQueryable interface:
IQueryable<Log> matches = m_Locator.Logs;
// Users filter
if (usersFilter)
matches = matches.Where(l => l.UserName == comboBoxUsers.Text);
// Severity filter
if (severityFilter)
matches = matches.Where(l => l.Severity == comboBoxSeverity.Text);
Logs = (from log in matches
orderby log.EventTime descending
select log).ToList();
That builds up the query before hitting the database. The command won't run until .ToList() at the end.
I solved this with an extension method to allow LINQ to be conditionally enabled in the middle of a fluent expression. This removes the need to break up the expression with if statements.
.If() extension method:
public static IQueryable<TSource> If<TSource>(
this IQueryable<TSource> source,
bool condition,
Func<IQueryable<TSource>, IQueryable<TSource>> branch)
{
return condition ? branch(source) : source;
}
This allows you to do this:
return context.Logs
.If(filterBySeverity, q => q.Where(p => p.Severity == severity))
.If(filterByUser, q => q.Where(p => p.User == user))
.ToList();
Here's also an IEnumerable<T> version which will handle most other LINQ expressions:
public static IEnumerable<TSource> If<TSource>(
this IEnumerable<TSource> source,
bool condition,
Func<IEnumerable<TSource>, IEnumerable<TSource>> branch)
{
return condition ? branch(source) : source;
}
When it comes to conditional linq, I am very fond of the filters and pipes pattern.
http://blog.wekeroad.com/mvc-storefront/mvcstore-part-3/
Basically you create an extension method for each filter case that takes in the IQueryable and a parameter.
public static IQueryable<Type> HasID(this IQueryable<Type> query, long? id)
{
return id.HasValue ? query.Where(o => i.ID.Equals(id.Value)) : query;
}
Doing this:
bool lastNameSearch = true/false; // depending if they want to search by last name,
having this in the where statement:
where (lastNameSearch && name.LastNameSearch == "smith")
means that when the final query is created, if lastNameSearch is false the query will completely omit any SQL for the last name search.
Another option would be to use something like the PredicateBuilder discussed here.
It allows you to write code like the following:
var newKids = Product.ContainsInDescription ("BlackBerry", "iPhone");
var classics = Product.ContainsInDescription ("Nokia", "Ericsson")
.And (Product.IsSelling());
var query = from p in Data.Products.Where (newKids.Or (classics))
select p;
Note that I've only got this to work with Linq 2 SQL. EntityFramework does not implement Expression.Invoke, which is required for this method to work. I have a question regarding this issue here.
It isn't the prettiest thing but you can use a lambda expression and pass your conditions optionally. In TSQL I do a lot of the following to make parameters optional:
WHERE Field = #FieldVar OR #FieldVar IS NULL
You could duplicate the same style with a the following lambda (an example of checking authentication):
MyDataContext db = new MyDataContext();
void RunQuery(string param1, string param2, int? param3){
Func checkUser = user =>
((param1.Length > 0)? user.Param1 == param1 : 1 == 1) &&
((param2.Length > 0)? user.Param2 == param2 : 1 == 1) &&
((param3 != null)? user.Param3 == param3 : 1 == 1);
User foundUser = db.Users.SingleOrDefault(checkUser);
}
I had a similar requirement recently and eventually found this in he MSDN.
CSharp Samples for Visual Studio 2008
The classes included in the DynamicQuery sample of the download allow you to create dynamic queries at runtime in the following format:
var query =
db.Customers.
Where("City = #0 and Orders.Count >= #1", "London", 10).
OrderBy("CompanyName").
Select("new(CompanyName as Name, Phone)");
Using this you can build a query string dynamically at runtime and pass it into the Where() method:
string dynamicQueryString = "City = \"London\" and Order.Count >= 10";
var q = from c in db.Customers.Where(queryString, null)
orderby c.CompanyName
select c;
You can create and use this extension method
public static IQueryable<TSource> WhereIf<TSource>(this IQueryable<TSource> source, bool isToExecute, Expression<Func<TSource, bool>> predicate)
{
return isToExecute ? source.Where(predicate) : source;
}
Just use C#'s && operator:
var items = dc.Users.Where(l => l.Date == DateTime.Today && l.Severity == "Critical")
Edit: Ah, need to read more carefully. You wanted to know how to conditionally add additional clauses. In that case, I have no idea. :) What I'd probably do is just prepare several queries, and execute the right one, depending on what I ended up needing.
You could use an external method:
var results =
from rec in GetSomeRecs()
where ConditionalCheck(rec)
select rec;
...
bool ConditionalCheck( typeofRec input ) {
...
}
This would work, but can't be broken down into expression trees, which means Linq to SQL would run the check code against every record.
Alternatively:
var results =
from rec in GetSomeRecs()
where
(!filterBySeverity || rec.Severity == severity) &&
(!filterByUser|| rec.User == user)
select rec;
That might work in expression trees, meaning Linq to SQL would be optimised.
Well, what I thought was you could put the filter conditions into a generic list of Predicates:
var list = new List<string> { "me", "you", "meyou", "mow" };
var predicates = new List<Predicate<string>>();
predicates.Add(i => i.Contains("me"));
predicates.Add(i => i.EndsWith("w"));
var results = new List<string>();
foreach (var p in predicates)
results.AddRange(from i in list where p.Invoke(i) select i);
That results in a list containing "me", "meyou", and "mow".
You could optimize that by doing the foreach with the predicates in a totally different function that ORs all the predicates.

Categories