Passing whole function as lambda expression to LINQ - c#

I've getting auctions from my database as:
var auctions = from o in db.auctions select o;
I'd like to pass this function as lambda expression in .Where clause of my linq to filter my auctions result:
private bool wordsHasProductName(auction a, string[] words)
{
if (!a.product_name.Contains(' ')) // Auction product name is single word - check if
{
if (words.Length > 1) return false; // array is passed, single word is searching
else return a.product_name.Contains(words[0]); // check if product name consists passed string
}
else // Auction product name has more words check if passed string is partially in this product name
{
string[] productName = a.product_name.Split(' ');
var list = new List<string>(words);
foreach (string item in productName)
{
if (list.Contains(item)) list.Remove(item);
}
return list.Count == 0;
}
}
I'm calling it in my code, and while debugging there's no error at this line, here's place where I call .Where() clause in linq:
string[] words = searchName.Split(' ');
auctions = auctions.Where((a) => wordsHasProductName(a, words));
But at the end at return statement I'm getting exception error:
return View(auctions.ToPagedList(pageNumber, pageSize));
Error code:
An exception of type 'System.NotSupportedException' occurred in
EntityFramework.SqlServer.dll but was not handled in user code
Additional information: LINQ to Entities does not recognize the method
'Boolean wordsHasProductName(IEP_Projekat.Models.auction,
System.String[])' method, and this method cannot be translated into a
store expression.
How could I succeed sending full bool function as lambda expression in my linq .Where()?

As others already mentioned, method calls cannot be used in LINQ to Entities because they cannot be translated to SQL. So the only possible way would be if you can rewrite the method as compatible expression. And here comes the other problem - you are using string.Split method which is not supported.
So you are out of luck. Or may be not. Here is the trick. Instead of splitting the product_name and checking if it contains every word in words, you can use the following (IMO equivalent) criteria:
words.All(word => (" " + product_name + " ").Contains(" " + word + " "))
Enclosing both product_name and word with space allows you to match the whole word at the beginning, middle or at the end of the target string. And it uses only constructs that are supported by the EF.
Putting it all together. The function would be like this:
private static Expression<Func<auction, bool>> wordsHasProductName(string[] words)
{
if (words.Length == 1)
{
var word = words[0]; // Avoid ArrayIndex not supported
return a => !a.product_name.Contains(" ") && a.product_name.Contains(word);
}
else
{
return a => words.All(word => (" " + a.product_name + " ").Contains(" " + word + " "));
}
}
and the usage:
string[] words = searchName.Split(' ');
auctions = auctions.Where(wordsHasProductName(words));
However the above implementation does not produce very good SQL query, especially for more than one word. A much better SQL is produced if rewriting it by building the predicate expression manually:
private static Expression<Func<auction, bool>> wordsHasProductName(string[] words)
{
Expression<Func<auction, string>> product_name;
Expression condition;
var containsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) });
if (words.Length == 1)
{
// a => !a.product_name.Contains(" ") && a.product_name.Contains(words[0])
product_name = a => a.product_name;
condition = Expression.AndAlso(
Expression.Not(Expression.Call(product_name.Body, containsMethod, Expression.Constant(" "))),
Expression.Call(product_name.Body, containsMethod, Expression.Constant(words[0])));
}
else
{
// a => (" " + a.product_name + " ").Contains(" " + words[0] + " ")
// && (" " + a.product_name + " ").Contains(" " + words[1] + " ")
// ...
// && (" " + a.product_name + " ").Contains(" " + words[N-1] + " ")
product_name = a => " " + a.product_name + " ";
condition = words
.Select(word => Expression.Call(product_name.Body, containsMethod, Expression.Constant(" " + word + " ")))
.Aggregate<Expression>(Expression.AndAlso);
}
return Expression.Lambda<Func<auction, bool>>(condition, product_name.Parameters);
}
Welcome to the world of EF and expressions :)

You can't pass this complex C# logic as an Expression Tree that can interpreted by the EF provider and the hardest part for the provider is transforming it into SQL statements.
So, your option is to create a stored procedure where you pass in the required parameters, then you write the logic using pure SQL. Then, you map your SP to EF and call it to return the list of objects you want.

Related

If a field contains null then entire concatenate result is null

I am trying to concatenate for a label. If a field contains null then entire concatenate result is null.
[HttpGet]
public ActionResult PMAByPC(string PartnerCode)
{
var result = (from N in _POSContext.PMAs
where (N.PartnerCode == PartnerCode)
select new
{
label = N.Address1 + " | " + N.Address2 + " | " + N.City,
id = N.ID
});
return Json(result);
}
Here, if data is not present in of the fields, label becomes null.
I tried with
select new { label = N.Address1 ?? "?" + " | " + N.Address2 ?? "?" + " | " + N.City ?? "?", id = N.ID }
then it takes only N.Address1 value and ignores the rest of the fields.
Looks like this is a standard SQL string concatenation behavior (the same happens with SqlServer database).
If you want to evaluate the concatenation server side (database), you need to convert null to empty string "" (or something else) using the ?? operator. Similar to your attempt, but you've missed the C# operator precedence. The way you wrote it is equivalent of
N.Address1 ??
(
("?" + " | " + N.Address2) ??
(
("?" + " | " + N.City) ?? "?"
)
)
which is not what was the intent.
You can avoid such issues by enclosing similar conversions with brackets:
select new
{
label = (N.Address1 ?? "?") + " | " + (N.Address2 ?? "?") + " | " + (N.City ?? "?"),
id = N.ID,
}
This is the standard compliant and reasonable behavior: if you concatenate a string with an unknown string, the result is unknown.
Use the coalesce function for that:
coalesce(col1, '') || coalesce(col2, '')

What is the replacement Sql Char(13) and isnull in Entity Framework?

I am new to Entity framework, Can any body tell me how to write the following query into Entity framework.
select column1 + char(13) +isnull(column2,space(1))+char(13)+isnull(column3,space(1))+char(13)+isnull(column4,space(1)) +char(13)+isnull(olumn5,space(1)) as FADRS
FROM table
Convert the above query into Entity Framework.
By using Jon answer i get the answer. Know my problem is how to use iqueryable
IQuer<string> Madr = from b in context.table
where b.column1 == txtaddrss.Text
select new
{FADR = b.column2 + '\r' +
(b.column3 ?? " ") + '\r' +
(b.column4 ?? " ") + '\r' +
(b.column5 ?? " ") + '\r' +
(b.column6 ?? " ")};
foreach(string something in Madr)
{
MessageBox.Show(something);
}
i am getting error conversion failed because of anonymous type
char(13) just does the equivalent (though more limited) of (char)13 in C#, which would just return '\r'.
Hence you would either use '\r' or "\r".
isnull(x, y)just does the equivalent of x ?? y in C#.
So you would use something like:
var query = from item in TableSource select
item.column1 + '\r' +
(item.column2 ?? " ") + '\r' +
(item.column3 ?? " ") + '\r' +
(item.column4 ?? " ") + '\r' +
(item.column5 ?? " ");
TableSource is whatever way you are getting a reference to the table (context.Table or whatever).
query will be an IQueryable<string> returning the relevant strings when invoked. If you really want the FADRS name from your example then the following will instead of strings return anonymous objects with a FADRS property:
var query = from item in TableSource select
new {FADRS = item.column1 + '\r' +
(item.column2 ?? " ") + '\r' +
(item.column3 ?? " ") + '\r' +
(item.column4 ?? " ") + '\r' +
(item.column5 ?? " ")};
Edit:
The first example above can be used as:
foreach(string something in query)
MessageBox.Show(something);
The second example as:
foreach(var something in query)
MessageBox.Show(something.FADR);
With the first var is optional shorthand, with the second you must use var as the types involved are anonymous, and hence var is the only way to name the type (by not naming it at all).
Given no further context I'd say sonething like this:
var query = from obj in context.table
select new {
FADR = obj.column1 + "\r" +
obj.column2 ?? " " + "\r" +
obj.column3 ?? " " + "\r" +
obj.column4 ?? " " + "\r" +
obj.column5 ?? " " + "\r" };

Concatenating IEnumerable<KeyValuePair<string,string>> values into string using Linq

Given IEnumerable<KeyValuePair<string,string>>, I'm trying to use linq to concatenate the values into one string.
My Attempt:
string path = attributes.Aggregate((current, next) => "#" + current.Key + "=" + current.Value + " and #" + next.Key + "=" + next.Value);
This produces the error:
Cannot convert expression type 'string' to return type 'KeyValuePair<string,string>'
Is there a more effiecient way to do this in linq?
The full method...
public IEnumerable<XmlNode> GetNodes(IEnumerable<KeyValuePair<string,string>> attributes) {
StateInfoXmlDocument stateInfoXmlDocument = new StateInfoXmlDocument();
string path = attributes.Aggregate((current, next) => "#" + current.Key + "=" + current.Value + " and #" + next.Key + "=" + next.Value);
string schoolTypeXmlPath = string.Format(SCHOOL_TYPE_XML_PATH, path);
return stateInfoXmlDocument.SelectNodes(schoolTypeXmlPath).Cast<XmlNode>().Distinct();
}
Is this what you're looking for?
var strings = attributes.Select(kvp => string.Format("#{0}={1}", kvp.Key, kvp.Value));
string path = string.Join(" and ", strings);
string s = String.Join("#",attributes.Select(kv=>kv.Key+"="+kv.Value));
If you want to use aggregate to make a string you need to use the seeded overload of aggregate
if you use the none seeded version then all the types in the call need to be the same.
string templ = "{0}={1}";
string _authStr = String.Join("&", formParams.Select(kv => String.Format(templ, kv.Key, kv.Value));

Linq to sql querying multiple columns - query help

I have a text field on my screen where users can search for a few things from one input field:
Address
Postcode
Client name
ID
What is the best way to write a query that will query these columns?
How should I take the input in, should I split string on space? or comma?
User input = 67 pottors lane 99
where "67 pottors lane" is address
"99" is/could be ID
What I have so far:
//q = querystring
//filteredlist = extension method for the table that I am querying
//clientlist = list of clients I have passed in as a param
//Here is my query for
if (!string.IsNullOrEmpty(q))
{
var result = q.Trim();
filteredlist = filteredlist
.Where(x => x.ID.Contains(q) || x.SubjectPropertyAddress1.Contains(q)
|| x.SubjectPropertyPostCode.Contains(q)
|| clientlist.Any(y=> x.ClientID == y.ClientID && (y.ID.Contains(q) || y.Name.Contains(q)) ));
}
NOTE: I will make use of indexing using sql.
Perhaps an idea: make an extra column in your database with all the columns you want to search for as one big concattenated string. Then do a free text search of your input string versus that field in the DB.
Since L2SQL does not support free text search, you need to do it in stored procedure in that case or do like here (http://stackoverflow.com/questions/67706/linqtosql-and-full-text-search-can-it-be-done)
Ouch - is there no way for you to split the input into separate fields for ID, Address, Postcode and Name?
If so, you can keep appending Where clauses, like
var query = filteredList;
if (!String.IsNullOrEmpty(id))
{
query = query.Where(c => c.ID.Contains(id))
}
if (!String.IsNullOrEmpty(name))
{
query = query.Where(c => c.Name.Contains(name))
}
.. same for Name, address etc
Otherwise, your query resembles more of a search engine query than a RDBMS query.
I have done a very similar thing in the past. I split the search string on the space character and restricted it to a maximum of 6 search "words", so that the linq is still manageable.
I came up with something like the following:
string[] words = q.ToLower().Split(' ');
string[] wordsFixed = new string[] {"", "", "", "", "", "" };
for(int i = 0; i < 6 && i < words.Length; i++)
wordsFixed[i] = words[i];
var data = from item in list
where (item.ID + item.Name + item.Address1 + item.Address2 + item.PostCode).ToLower().Contains(wordsFixed[0]) &&
(item.ID + item.Name + item.Address1 + item.Address2 + item.PostCode).ToLower().Contains(wordsFixed[1]) &&
(item.ID + item.Name + item.Address1 + item.Address2 + item.PostCode).ToLower().Contains(wordsFixed[2]) &&
(item.ID + item.Name + item.Address1 + item.Address2 + item.PostCode).ToLower().Contains(wordsFixed[3]) &&
(item.ID + item.Name + item.Address1 + item.Address2 + item.PostCode).ToLower().Contains(wordsFixed[4]) &&
(item.ID + item.Name + item.Address1 + item.Address2 + item.PostCode).ToLower().Contains(wordsFixed[5])
select item;

Build a linq query based on specific criteria c#

I have a List ListPeople "list of people named ListPeople" and the class for an object People is:
class People
{
public string Name {get;set;}
public DateTime Dob {get;set;}
public int Wieght {get;set;}
}
How could I perform a search with criteria chosen by user: Something like:
for example if the user would chose something like:
Then I would know how to set up that query:
var query = (from a in ListPeople
where a.Name == "Tom" &&
a.Weight > 25 &&
a.Dob < "dateTime.Now() - 7 months" // you know what I mean
select a).ToList();
do I have to build 4*4*4 (all posible combinations) number of queries?
You do not need to build all possible combinations ahead of time, you just need the ability to keep building upon your query. A rough draft:
var myQuery = ListPeople.AsEnumerable();
if (name.Selection == "Is")
myQuery = myQuery.Where(p => p.Name == nameInput.Text);
else if (name.Selection == /* continues */
You can continue doing this for each of your UI elements to build appropriate predicates for your query and then after you're done, evaluate it as normal.
You can do the same thing for Linq-to-SQL or EF, you just want to use AsQueryable() instead of AsEnumerable() so you can finish the query before sending it against the database.
var myQuery = context.People.AsQueryable();
// continues
In order to do this with LINQ, you'd need to pull all the data, then write separate where clauses to filter out what you need. You would pass all variables to the function as a string so that you can easily tell what is empty. This is the function you would setup:
public List<ListPeople> GetPeopleList(string Name, string opName, string Weight, string opWeight, string DOB, string opDOB)
{
var items = from a in ListPeople
select a;
//--- repeat the section below for Weight and DOB
if (!string.IsNullOrEmpty(Name))
{
switch(opName.ToLower())
{
case "contains":
{
items = items.Where(a => SqlMethods.Like(a.Name, "%" + Name + "%"));
break;
}
case "does not contain":
{
items = items.Where(a => !SqlMethods.Like(a.Name, "%" + Name + "%"));
break;
}
case "is":
{
items = items.Where(a => a.Name == Name));
break;
}
case "is not":
{
items = items.Where(a => a.Name != Name));
break;
}
}
}
//--- end repeat
return items.ToList();
}
Good Luck!
EDIT:
Since my answer here, I have found a better way to do these types of queries and it will dramatically increase the performance. Checkout http://weblogs.asp.net/scottgu/archive/2008/01/07/dynamic-linq-part-1-using-the-linq-dynamic-query-library.aspx
This class allows you to build your LINQ query dynamically in a string format and then pass it to the query. Here is an example of how I have used it in a Real Estate website on the property search function (slimmed down for ease):
public IQueryable GetSearchResults(string PriceFrom, string PriceTo, string Beds, string Baths, string SqftFrom, string SqftTo, string YearFrom, string YearTo)
{
DatabaseDataContext db = new DatabaseDataContext();
string WhereClause = string.Empty;
if (!string.IsNullOrEmpty(PriceFrom))
WhereClause = "ListPrice >= " + PriceFrom + " AND ";
if (!string.IsNullOrEmpty(PriceTo))
WhereClause += "ListPrice <= " + PriceTo + " AND ";
if (!string.IsNullOrEmpty(Beds))
WhereClause += "Beds >= " + Beds + " AND ";
if (!string.IsNullOrEmpty(Baths))
WhereClause += "FullBaths >= " + Baths + " AND ";
if (!string.IsNullOrEmpty(SqftFrom))
WhereClause += "SqFtHeated >= " + SqftFrom + " AND ";
if (!string.IsNullOrEmpty(SqftTo))
WhereClause += "SqFtHeated <= " + SqftTo + " AND ";
if (!string.IsNullOrEmpty(YearFrom))
WhereClause += "YearBuilt >= " + YearFrom + " AND ";
if (!string.IsNullOrEmpty(YearTo))
WhereClause += "YearBuilt <= " + YearTo + " AND ";
if (WhereClause.EndsWith(" AND "))
WhereClause = WhereClause.Remove(WhereClause.Length - 5);
IQueryable query = db.Listings
.Where(WhereClause)
.OrderBy("ListPrice descending");
return query;
}
Good Luck!
Have a look at predicate builder.

Categories

Resources