Build a linq query based on specific criteria c# - 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.

Related

Passing whole function as lambda expression to LINQ

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.

How to get same functionality as ISNULL in LINQ query

I have been trying to find a solution for this since part few hours and I feel like my brain is about to help. I have the below LINQ query.
DropDownListItem item = (
from c in context.Practitioners
where c.PractitionerID == id
select new DropDownListItem
{
Id = c.PractitionerID,
DisplayValue = c.FirstName + " " + c.MiddleName + " " + c.LastName,
IsActive = c.IsActive,
DisplayOrder = c.PractitionerID,
CreatedById = new Guid("COFFEEOO-LOVE-LIFE-LOVE-C0FFEEC0FFEE"),
CreatedDate = c.CreatedDate,
}).FirstOrDefault() ?? new DropDownListItem();
response.Data = item;
There are instaces when c.MiddleName could be null. How Can I handle it in this query so that if c.MiddleName is null I can just assign a blank "" string to it ?
Here's what I tried already which did not work out.
- Created extension to check IsNullOrEmpty. I found out this does not work on LINQ queries.
- tried converting c.Middle to string by doing something like c.MiddleName.ToString() which did not work for LINQ query either.
Please give me more more direction as to which I should move toward. Thanks!
You can check for nulls and empty strings instead of using any methods that LINQ to Entities does not understand (Trim will be translated to SQL):
DisplayValue = c.FirstName + " " + ((c.MiddleName == null || c.MiddleName.Trim() == string.Empty) ? string.Empty : (c.MiddleName + " ")) + c.LastName,
I'm a bit confused about your question. You are making a string, so even if c.MiddleName is null it should be interpreted as an empty string.
You can also try this:
DisplayValue = (c.MiddleName != null) ?
c.FirstName + " " + c.MiddleName + " " + c.LastName :
c.FirstName + " " + c.LastName,
But pretty much all other answers are very similar.
As a note, you are missing brackets behind select new DropDownListItem, so that might be problem.
I'll assume what you mean by c.MiddleName is empty is that c.MiddleName is null (because if it's an empty string you question makes no sense :p).
If it is indeed the case try writing c.MiddleName ?? ""
This means that if the left part of ?? is null the use the right part of the expression
See https://msdn.microsoft.com/en-us/library/ms173224.aspx for more documentation
And you would have to change your code in this fashion :
DropDownListItem item = (
from c in context.Practitioners
where c.PractitionerID == id
select new DropDownListItem
{
Id = c.PractitionerID,
DisplayValue = (c.FirstName ?? "") + " " + (c.MiddleName ?? "") + " " + (c.LastName ?? ""),
IsActive = c.IsActive,
DisplayOrder = c.PractitionerID,
CreatedById = new Guid("COFFEEOO-LOVE-LIFE-LOVE-C0FFEEC0FFEE"),
CreatedDate = c.CreatedDate,
}).FirstOrDefault() ?? new DropDownListItem();
response.Data = item;
To be noted that the ?? operator have one of the lowest priorities in C#.
Check the operators priority in C# here https://msdn.microsoft.com/en-us/library/6a71f45d.aspx
try this
DisplayValue = (c.FirstName?? string.Empty) + " " + (c.MiddleName ?? string.Empty) + " " + (c.LastName?? string.Empty)

Search data gridview using textbox and checkboxlist asp.net c#

I want to use textbox and checkboxlist to search data in gridview using asp.net c#. Using textbox can search the data. But for checkboxlist only can search the data when check only one checkbox. If check more than one checkbox, can't search the data. thanks a lot for helping.
the code:
c# code
protected void btnSearch_Click(object sender, EventArgs e)
{
if (cblDay.SelectedIndex != -1)
{
foreach (ListItem val in cblDay.Items)
{
if (val.Selected == true)
{
RptRateData.Day += val.Value + "";
}
}
}
RptRateData.RateAmount = txtRate.Text.Trim();
BindGrid();
}
code for class:
public string RateAmount { get; set; }
public string Day { get; set; }
internal DataSet GetRptRateSet()
{
DataSet tmpDS = new DataSet();
try
{
string strSQL = #"SELECT ComplexRateInfo.ComplexRateId,
ComplexRateDetailInfo.Day,
ComplexRateInfo.RateAmount
FROM ComplexRateInfo
LEFT JOIN ComplexRateDetailInfo ON ComplexRateInfo.ComplexRateId = ComplexRateDetailInfo.ComplexRateId ";
string whereSQL = " WHERE";
string orderBySQL = " order by Day ;";
int filterCount = 0; //to keep track of needed filter that are going to be used by the sql string
string[] sIndex = new string[2]; //to keep track of scalar variable needed by the sql, four string of sIndex because maximum filter available is 4
int indexCount = 0; //count to access sIndex members
//filter with or without day
if (_ds.Day != null && _ds.Day != "")
{
if (filterCount > 0) //query need additional filter
whereSQL = whereSQL + " AND ComplexRateDetailInfo.Day LIKE '{" + filterCount + "}'";
else //if this is the first filter
whereSQL = whereSQL + " ComplexRateDetailInfo.Day LIKE '{" + filterCount + "}'";
filterCount++;
sIndex[indexCount] = _ds.Day;
indexCount++;
}
//filter with or without rate amount
if (_ds.RateAmount != null && _ds.RateAmount != "")
{
if (filterCount > 0) //query need additional filter
whereSQL = whereSQL + " AND ComplexRateInfo.RateAmount LIKE '{" + filterCount + "}'";
else //if this is the first filter
whereSQL = whereSQL + " ComplexRateInfo.RateAmount LIKE '{" + filterCount + "}'";
filterCount++;
sIndex[indexCount] = _ds.RateAmount;
indexCount++;
}
//build complete query with no filter at all
if (filterCount == 0)
{
strSQL = strSQL + orderBySQL;
tmpDS = Db.GetDataSet(string.Format(strSQL));
}
//build complete query with 1 or more filter
else
{
strSQL = strSQL + whereSQL + orderBySQL;
tmpDS = Db.GetDataSet(string.Format(strSQL, sIndex));
}
}
catch (Exception ex)
{
throw ex;
}
return tmpDS;
}
There are two mistakes in your code.
Assigning values to RptRateData.Day from CheckBoxList.
Description: You assign selected values to object without using any separator. So For example if you have selected 1, 2, 4 days then as per your code, value of RptRateData.Day will be 124. Instead of that, it should be separated with comma as shown below:
var selectedDays = string.Empty;
foreach (ListItem val in cblDay.Items)
{
if (val.Selected == true)
{
selectedDays += "'" + val.Value + "',";
}
}
RptRateData.Day = selectedDays.TrimEnd(new char[] { ',' });
Now come to the second point which is in your SQL query which you make dynamically.
Description: In this query in WHERE clause you use Like operator for ComplexRateDetailInfo.Day. This will not work anymore. Instead of that you should use IN operator.
Note: Are you sure that your Like operator is working with curly braces ('{' and '}') and without '%' symbol ?

How do you display items from a list in a message box?

I am working on a project that needs to display a list of people that have above average income. The source data is a List<IncomeData> (id is the unique id for the person):
public struct IncomeData
{
public string id;
public double household;
public income;
}
public double belowAverage = 0, total, belowAveragePercent;
IncomeData surveyStruct;
List<IncomeData> surveyList = new List<IncomeData>();
List<string> aboveAverage = new List<string>();
Here is how I determine if a person has above average income. If a person has above average income, I add the id and income from the temporary instance of the surveyStruct to the above average list of string values:
//Determine poverty.
if (surveyStruct.income - 3480 * surveyStruct.household <= 6730)
{
belowAverage += 1;
}
else if (surveyStruct.income - 3480 * surveyStruct.household >= 6730)
{
aboveAverage.Add(surveyStruct.id);
aboveAverage.Add(surveyStruct.income.ToString());
}
And here is the code that displays the desired information in a message box. (The aboveAverage list is added in here, too.)
private void reportsToolStripMenuItem_Click(object sender, EventArgs e)
{
//Display reports 1, 2, and 3.
MessageBox.Show("Your Entry:\nID Code: " + surveyStruct.id +
"\nHousehold: " + surveyStruct.household.ToString() +
" people\nIncome: " + surveyStruct.income.ToString("C") +
"\n\nPeople Above Average:\n" + aboveAverage +
"\n\nAnd " + belowAveragePercent + "% of people are below average.");
}
Now, here's the problem: Instead of a seeing a list of values in the message box, I am seeing System.Collections.Generic.List`1[System.String] where the IDs and incomes of the above average people should be. Can somebody please tell me what I am doing wrong and how I can display a list values in a message box?
At the end of your question you ask: How I can display a List<IncomeData> in a message box?
So, the core of your question is converting your list of values to a string so that you can pass that string as an argument to MessageBox.Show().
The LINQ Extension Method Enumerable.Aggregate() offers an ideal solution for this problem. Say your List<IncomeData> looks something like this (I've omitted the household field for brevity):
var incomes = new List<IncomeData>() {
new IncomeData("abc0123", 15500),
new IncomeData("def4567", 12300),
new IncomeData("ghi8901", 17100)
};
The following LINQ query will convert that List<IncomeData> into a string:
string message = incomes.
Select(inc => inc.ToString()).
Aggregate((buffer, next) => buffer + "\n" + next.ToString());
To eliminate the need to call Select(), you can instead use the two-argument version of Enumerable.Aggregate(). This approach also allows you to specify a heading as the seed value for your accumulator:
string message2 = incomes.
Aggregate(
"Income data per person:",
(buffer, next) => buffer + "\n" + next.ToString());
That is equivalent to the following where the argument types have been made explicit:
string message = incomes.
Aggregate<IncomeData, string>(
"Income data per person:",
(string buffer, IncomeData next) => buffer + "\n" + next.ToString());
See the following (and online demo) for a complete working example preceded by its expected output.
Expected Output
Income data per person:
Id: abc0123, Income:15500
Id: def4567, Income:12300
Id: ghi8901, Income:17100
Demonstration Program
using System;
using System.Collections.Generic;
using System.Linq;
namespace LinqAggregateDemo
{
public class Program
{
public static void Main(string[] args)
{
var incomes = new List<IncomeData>() {
new IncomeData("abc0123", 15500),
new IncomeData("def4567", 12300),
new IncomeData("ghi8901", 17100)
};
string message = incomes.
Select(inc => inc.ToString()).
Aggregate((buffer, next) => buffer + "\n" + next.ToString());
Console.WriteLine("Income data per person:\n" + message);
}
public struct IncomeData
{
public readonly string Id;
public readonly int Income;
public IncomeData(string id, int income)
{
this.Id = id;
this.Income = income;
}
public override string ToString()
{
return String.Format(
"Id: {0}, Income:{1}",
this.Id,
this.Income);
}
}
}
}
First, make aboveAverage a List<IncomeData>, and add the IncomeDatas that match into that list.
Then, you need to define a ToString for your custom struct, something like this:
public override void string ToString()
{
return string.Format("The id is {0}, the household is {1} and the income is {2}.", id, household, income);
}
Then, in your MessageBox.Show call, you need to replace aboveAverage with
aboveAverage.Aggregate((a,b) => a.ToString() + Enviroment.NewLine + b.ToString())
Should make it show properly.
Sorry about the formatting,I'm on mobile.
StringBuilder is one choice:
StringBuilder aboveAverage = new StringBuilder();
//Determine poverty.
if (surveyStruct.income - 3480 * surveyStruct.household <= 6730)
{
belowAverage += 1;
}
else if (surveyStruct.income - 3480 * surveyStruct.household >= 6730)
{
aboveAverage.Append(string.Format("id: %s, income: %s\n",
surveyStruct.id, surveyStruct.income.ToString());
}
And you will need a ToString() for the string builder, like this:
MessageBox.Show("Your Entry:\nID Code: " + surveyStruct.id + "\nHousehold: " + surveyStruct.household.ToString() + " people\nIncome: " + surveyStruct.income.ToString("C") + "\n\nPeople Above Average:\n" + aboveAverage.ToString() + "\n\nAnd " + belowAveragePercent + "% of people are below average.");
You could do it with join if you leave aboveAverage as a list, like this:
string.Join(aboveAverage,Environment.NewLine);
In your current code -- but that would not look so nice.
You could also do it with Linq, you want to see that?
Ok, here is a sexy one line version: (all questions should have a one line linq answer):
(the using and indent don't count, they are just there to make the code more readable!)
using NL = Environment.NewLine;
    
string indent = "    ";
MessageBox.Show(
"Your Entry:" + NL +
"ID Code: " + surveyStruct.id + NL +
"Household: " + surveyStruct.household.ToString() + " people" + NL +
"Income: " + surveyStruct.income.ToString("C") + NL + NL +
"People Above Average:" + NL +
indent + string.Join(NL+indent,
surveyList.Where(s => (s.income - 3480) * s.household >= 6730)
.Select(s => "ID: "+s.id+" $"+s.income.ToString).ToArray()) + NL +
"And " + (surveyList.Where(s => ((s.income - 3480) * s.household) <= 6730).Count() / surveyList.Count()) * 100 + "% of people are below average.");

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;

Categories

Resources