LINQ method with varying parameters - c#

I have a LINQ method for the Search page of an in house app. The method looks as below
public static DataTable SearchForPerson(String FirstName, String MiddleName, String LastName, String SSN, DateTime? BirthDate)
{
var persons = (from person in context.tblPersons
where person.LastName == LastName || person.LastName.StartsWith(LastName)
join addresse in context.tblAddresses on person.PersonID equals addresse.PersonID
orderby person.LastName
select new { person.PersonID, person.LastName, person.FirstName, person.SSN, addresse.AddressLine1 });
var filteredPersonsList = persons.Where(p => p.LastName == LastName).ToList();
if (filteredPersonsList.Count == 0)
filteredPersonsList = persons.Where(p => p.LastName.StartsWith(LastName)).ToList();
var dataTable = filteredPersonsList.CopyLinqToDataTable();
return dataTable;
}
Now, as you can no doubt see, I made a slight oversight when creating this as it only searches by LastName. I was in process of expanding this when it occured to me that I may not be going about this properly.
So, finally to my question; Is it more desirable(read-best practice, more efficient, etc...) to have a single method like this with a mechanism(I am thinking a SWITCH on non-empty param) to tell which parameter to search with or should I simply make multiple versions, a la SearchForPersonByLastName & SearchForPersonBySSN?
Additionally, is there an even more elagent solution to this, I would think common, issue?

Am I understanding correctly that only one of the parameters will be used to do the search? If so then absolutely these should be separate methods. Any time you describe a method (or class etc.) using the word "and" or "or" you probably have a method that can be broken into multiple methods. So it sounds like this method is currently described as "this method searches for Persons by FirstName or MiddleName or LastName or SSN or BirthDate." So, write methods
SearchByFirstName
SearchByMiddleName
SearchByLastName
SearchBySSN
SearchByBirthDate
Obviously there will be some common logic between these methods that you can factor out into a helper method.
Please clarify if I misunderstood and I'll edit my answer accordingly.
Edit:
Okay, so you say that you might be searching by multiple parameters. I still strongly prefer the idea of separate methods for each parameter (better separation of concerns, easier to maintain, easier to test, etc.). Here is one way to tie them all together:
DataTable Search(
string firstName,
string middleName,
string lastName,
string ssn,
DateTime? birthdate
) {
IQueryable<Person> query = context.tblPersons;
if(SearchParameterIsValid(firstName)) {
query = SearchByFirstName(query, firstName);
}
if(SearchParameterIsValid(middleName)) {
query = SearchByMiddleName(query, middleName);
}
if(SearchParameterIsValid(lastName)) {
query = SearchByLastName(query, lastName);
}
if(SearchParameterIsValid(ssn)) {
query = SearchBySSN(query, ssn);
}
if(birthDate != null) {
query = SearchByBirthDate(query, birthDate);
}
// fill up and return DataTable from query
}
bool SearchParameterIsValid(string s) {
return !String.IsNullOrEmpty(s);
}
IQueryable<Person> SearchByFirstName(
IQueryable<Person> source
string firstName
) {
return from p in source
where p.FirstName == firstName || p.FirstName.StartsWith(firstName)
select p;
}
// etc.
Or:
DataTable Search(
string firstName,
string middleName,
string lastName,
string ssn,
DateTime? birthdate
) {
Predicate<Person> predicate = p => true;
if(SearchParameterIsValid(firstName)) {
predicate = PredicateAnd(predicate, FirstNamePredicate(firstName));
}
if(SearchParameterIsValid(middleName)) {
predicate = PredicateAnd(predicate, MiddleNamePredicate(middleName));
}
// etc.
}
Predicate<T> PredicateAnd<T>(Predicate<T> first, Predicate<T> second) {
return t => first(t) && second(t);
}
Predicate<Person> FirstNamePredicate(string firstName) {
return p => p.FirstName == firstName || p.FirstName.StartsWith(firstName);
}
// etc.
DataTable SearchByPredicate(
IQueryable<Person> source,
Predicate<Person> predicate
) {
var query = source.Where(predicate)
.Join(
context.tblAddresses,
p => p.PersonID,
a => a.PersonID,
(p, a) => new {
p.PersonID,
p.LastName,
p.FirstName,
p.SSN,
a.AddressLine1
}
);
return query.CopyLinqToDataTable();
}

If I understand your question right, you're trying to add the other parameters to the where clause of your query. Might I suggest:
var persons = (from person in context.tblPersons
where (!string.IsNullOrEmpty(LastName) && (person.LastName == LastName || person.LastName.StartsWith(LastName))) &&
(!string.IsNullOrEmpty(SSN) && (person.SSN == SSN)) // && etc as needed
join addresse in context.tblAddresses on person.PersonID equals addresse.PersonID
orderby person.LastName
select new { person.PersonID, person.LastName, person.FirstName, person.SSN, addresse.AddressLine1 });
This would allow you to pass any combination of parameters to filter on so you're not locked in to filtering on one parameter.

The intent would be much more clear with multiple methods.
If I look at your code and you use just one method, I'd be able to figure out what was going on, but I'd have to look at it for a while to see what the heck you're doing. Maybe some comments would help clarify things, etc...
But, multiple methods will show me EXACTLY what you're trying to do.
As Jason said, be sure to factor out the common code into a helper method. I'd hate to see the same (more or less) linq query in each method.

You can add multiple where clauses so callers can specify the name fields they wish to search on, or null to match anything:
var filteredPersonsList = persons
.Where(p => FirstName != null && p.FirstName == FirstName)
.Where(p => MiddleName != null && p.MiddleName == MiddleName)
.Where(p => LastName != null && p.LastName == LastName).ToList();
So the caller could specifiy:
var matches = SearchForPerson("firstName", null, "lastName", "SSN", dob);
To ignore the middle name in the search.
Note you can combine these clauses into one using && although that could get difficult to read.

The single method you have is fine.
I would build up the LINQ one where clause at a time. This way when you actually run the LINQ it only processes the needed where clauses. This should be more efficient than other solutions proposed. LINQ is great as you can create your LINQ expression piece by piece using if logic as needed and then run it. You do not need to put all your if logic inside your LINQ expression if you can determine the logic while building the LINQ expression.
I would also simplify to only StartsWith.
One other thing, it seems that filteredPersonsList filtering is redundant as you are already filtering and I believe you can get rid of those lines.
var persons = from person in context.tblPersons
select person;
if (!string.IsNullOrEmpty(FirstName))
persons = from person in persons
where person.FirstName.StartsWith(FirstName)
select person;
if (!string.IsNullOrEmpty(MiddleName))
persons = from person in persons
where person.MiddleName.StartsWith(MiddleName)
select person;
if (!string.IsNullOrEmpty(LastName))
persons = from person in persons
where person.LastName.StartsWith(LastName)
select person;
if (!string.IsNullOrEmpty(SSN))
persons = from person in persons
where person.SSN = SSN
select person;
if (BirthDate.HasValue)
persons = from person in persons
where person.BirthDate == BirthDate.Value
select person;
return (from person in persons
join address in context.tblAddresses
on person.PersonID equals address.PersonID
orderby person.LastName
select new { person.PersonID, person.LastName,
person.FirstName, person.SSN, address.AddressLine1 })
.ToList()
.CopyLinqToDataTable();

Might want to create an object to reflect a person, then add a filter method to it:
Person.AddFilter( fieldToLimit, operator, value)
This way you can add any number of filter criteria to the object.
Example:
Person.AddFilter( FirstName, Contains, "Bob");
Person.AddFilter( LastName, StartsWith, "Z");
Another way is to simply add your criteria to your Linq to SQL IQueryable datatype so that you can simply:
Person.Where( t => t.FirstName.Contains("Bob")).Where( t => t.LastName.StartsWith("Z"));

Related

How do I use a variable with my WHERE clause in my Entity Framework query

I'm running a query in my project with multiple joins. I want to provide the WHERE clause with a variable instead of hard coded as it is now but cannot seem to get the correct syntax.
var data = (from a in db.StudentData
join b in db.Contacts on a.SID equals b.SID
join c in db.Addresses on a.SID equals c.SID
join d in db.EntryQuals.DefaultIfEmpty() on a.SID equals d.SID
where a.SID == searchTxt
select new
{
ID = a.SID,
Birthdate = a.BIRTHDTE,
FirstName = a.FNAMES,
PreviousName = a.PREVSURNAME,
EntryQualAwardID = d.ENTRYQUALAWARDID,
AwardDate = d.AWARDDATE
}).ToList();
How can I get my WHERE clause to work with a variable (ie: a.[ fieldVar ] ) where fieldVar could be "SID" as it is in the code currently.
When dealing with user select-able search criteria you will need to code for the possible selections. When dealing with building searches I recommend using the Fluent syntax over the Linq QL syntax as it builds an expression that is easy to conditionally modify as you go. From there you can use a Predicate & PredicateBuilder to dynamically compose your WHERE condition.
Jacques solution will work, but the downside of this approach is that you are building a rather large & complex SQL statement which conditionally applies criteria. My preference is to conditionally add the WHERE clauses in the code to ensure the SQL is only as complex as it needs to be.
If you want to do something like a smart search (think Google with one text entry to search across several possible fields)
var whereClause = PredicateBuilder.False<StudentData>();
int id;
DateTime date;
if(int.TryParse(searchTxt, out id))
whereClause = whereClause.Or(x => x.SID == id);
else if(DateTime.TryParse(searchTxt, out date))
whereClause = whereClause.Or(x => x.BirthDate == date);
else
whereClause = whereClause.Or(x => x.FirstName.Contains(searchTxt));
var data = db.StudentData
.Where(whereClause)
.Select(a => new
{
ID = a.SID,
Birthdate = a.BIRTHDTE,
FirstName = a.FNAMES,
PreviousName = a.PREVSURNAME,
EntryQualAwardID = a.EntryQuals.ENTRYQUALAWARDID,
AwardDate = a.EntryQuals.AWARDDATE
}).ToList();
This does some basic evaluations of the search criteria to see if it fits the purpose of the search. I.e. if they can search by name, date, or ID and IDs are numeric, we only search on an ID if the criteria was numeric. If it looked like a date, we search by date, otherwise we search by name. (and potentially other searchable strings)
If they can search for ID, FirstName, and BirthDate and enter one or more of those as separate entry fields (Search criteria page) then based on which entries they fill in you can either pass separate nullable parameters and do the above based on what parameters are passed, or pass a list of search values with something like an Enum for which value was searched for:
I.e. by parameters:
private void ByParameters(int? id = null, DateTime? birthDate = null, string name = null)
{
var whereClause = PredicateBuilder.False<StudentData>();
if(id.HasValue)
whereClause = whereClause.Or(x => x.SID == id.Value);
if(date.HasValue)
{
DateTime dateValue = date.Value.Date;
whereClause = whereClause.Or(x => x.BirthDate == dateValue);
}
if (!string.IsNullOrEmpty(name))
whereClause = whereClause.Or(x => x.FirstName.Contains(name));
// ....
}
If the number of parameters starts to get big, then a custom type can be created to encapsulate the individual null-able values. I.e.:
[Serializable]
public class SearchCriteria
{
public int? Id { get; set; }
public DateTime? BirthDate { get; set; }
public string Name { get; set; }
}
private void ByParameters(SearchCriteria criteria)
{
// ....
}
Or you can compose a more dynamic parameter list object with a criteria type and value but it starts getting more complex than it's probably worth.
You can't really do that in Linq, sine linq needs to know the the type of the field at compile time. A workaround would be something like
where (fieldVar=="SID" && a.SID == searchTxt) ||
(fieldVar=="FNAMES" && a.FNAMES== searchTxt) || ...
This will also alert you at compile time if you are doing an illegal comparison, eg. comparing a date to a string.

Advice regarding LINQ query for filtering collections

I have a List<T> of Customers that contains information like: Name, Product, Note, Booking Date, and UnreadMessage.
My goal is to filter customers using these fields and using AND operator but what's troubling me is when there is a field that is not used for filtering.
For example, an assistant will look for a customer name with a specific product. I could have a LINQ query that will look like this.
var a = Customers.Where(x => x.name.Contains("someone") && x.product.Contains("nike"));
Another example is, it will look for a customer with, with a specific product, with some note
var a = Customers.Where(x => x.name.Contains("someone") && x.product.Contains("nike") && x.note.Contains("some note"));
Another example, it will look for a product and booking date
var a = Customers.Where(x => x.product.Contains("someone") && x.bookingdate=DateTime.Now);
I hope you notice how many differenct queries I will write for this kind of filtering.
Name, product, note, booking date, or unread messages only
name and product
name and note
name and booking date
name and unread messages
product and note
product and booking date
etc etc etc etc
I am writing an Windows tablet application by the way so DataTable and LINQ Dyanmics are not possible where I can just write a string expression.
I am aksing for an advice and help how to solve this kind of filtering.
Why not combine Where?
var result = Customers
.Where(item => (null == name) || item.name.Contains(name))
.Where(item => (null == product) || item.product.Contains(product))
.Where(item => (null == note) || item.note.Contains(note))
...
So if you don't want to filter out by any parameter (name, product, etc.) just set it to null.
You can just build your statement dynamically. if this is linq to sql you will benefit from simpler execution plans with this approach:
public class test
{
public string name;
public string lastname;
}
class Program
{
static void Main(string[] args)
{
var list = new List<test>
{
new test{name = "john", lastname = "smith"}
};
string fname = "aa";
string lname = "sm";
var select = list.Select(c=>c);
if (fname != null)
select = select.Where(c => c.name.Contains(fname));
if (lname != null)
select = select.Where(c => c.lastname.Contains(lname));
var result = select.ToList();
}
}

Implement this "not in" where clause in LINQ

I have a table here where it gets popuplated with ActiveDirectory users every night. This list included generic AD accounts used for a variety of purposes.
Examples of lastnames of generic accounts:
vendor testing
IT support
Dept1 Printer
Visitor1
Visitor2
Guest1
Guest2 and etc
I want to retrieve all records ignoring these records. Something like
select * from table where lastname not like '%visitor%'
and lastname not like "%support%"
and so on I made this query but it does not do substring comparison.
List<String> _ignoreList = new List<String> { "visitor", "test" };
IQueryable<String> _records =
from _adUserDatas in _adUserDataDBDataContext.ADUserDatas
where
_adUserDatas.accountActive.ToLower().Contains("yes")
&& _adUserDatas.staffStudentType.ToLower().Contains("neither")
&& !_ignoreList.Contains(_adUserDatas.lastName)
orderby _adUserDatas.username
select _adUserDatas.username;
Here's the resulting SQL being sent to SQL Server.
{
SELECT[t0].[username]
FROM[dbo].[ADUserData] AS[t0]
WHERE
(LOWER([t0].[accountActive]) LIKE# p0)
AND
(LOWER([t0].[staffStudentType]) LIKE# p1)
AND
(NOT([t0].[lastName] IN(#p2, #p3)))
ORDER BY[t0].[username]
}
in LINQ query above, it did not ignore a record with the lastname "only for testing acct".
Any ideas on how to implement it using LINQ?
I've search the net but nothing came up.
Thanks a lot
That is because your are checking whether ignoreList contains the LastName, try doing it the other way.. i.e Whether LastName conatins anything from the ignoreList..
&& !_ignoreList.Any( il => _adUserDatas.lastName.Contains( il ) )
This way it will check whether "only for testing acct" contains anything from { "visitor", "test" }
Hm.. it could be hard to get to work like predicate with in clausule.. My solution would be other:
var queryable = from _adUserDatas in _adUserDataDBDataContext.ADUserDatas
where
_adUserDatas.accountActive.ToLower().Contains("yes")
&& _adUserDatas.staffStudentType.ToLower().Contains("neither")
orderby _adUserDatas.username
select _adUserDatas.username;
foreach (var ignore in _ignoreList)
{
var localIgnore = ignore;
queryable = queryable.Where(userName => !userName.Contains(localIgnore))
}
var result = queryable.ToList();
The answer from pwas lead me to one that works for my situation. PredicateBuilder which is mentioned in lots of topics here in SOF.com. http://www.albahari.com/nutshell/predicatebuilder.aspx
Here's the final code:
ADUserDataDBDataContext _adUserDataDBDataContext = new ADUserDataDBDataContext();
IQueryable<String> _records = null;
Expression<Func<ADUserData,Boolean>> _whereClause = PredicateBuilder.True<ADUserData>();
_whereClause = _whereClause.And(ADUserData => ADUserData.accountActive.ToLower().Contains("yes"));
foreach (var _item in _ignoreList)
{
_whereClause = _whereClause.And(ADUserData => !ADUserData.lastName.ToLower().Contains(_item));
}
_records = _adUserDataDBDataContext.ADUserDatas
.Where(_whereClause)
.Select(ADUserData => ADUserData.fan);
return _records.ToList();

Get element from a List<Class> of classes

I have a class called Employee that represents each employee in a company.
public class Employee
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public string Email { get; set; }
}
I have created a new List of Employees
public readonly List<Employee> Employees = new List<Employee>();
Now i want to find an employee from the list by his\her name but i don't know how to achieve that so your help will be very appreciated and thanks.
Using a LINQ-query:
Employees.FirstOrDefault(emp => emp.FirstName == "Rune" && emp.LastName == "S");
This will return you the first employee by that name, or if none are found, null. To get all employees with the name, use Where instead:
Employees.Where(emp => emp.FirstName == "Rune" && emp.LastName == "S");
The non-Linq approach using List.FindAll
List<Employee> foundEmployees = Employees
.FindAll(e => e.LastName.Equals(empName, StringComparison.OrdinalIgnoreCase));
Used the Equals approach to show you how to search ignoring the case.
If you're able to use LINQ, you could simply query the List:
Employees.Where(e => e.FirstName == "John" || e.LastName == "Doe");
Or, you could also quite easily loop through the list and add any matches to another resulting list:
List<Employee> matches = new List<Employee>();
foreach(var employee in Employees)
{
if(employee.FirstName == "John" || employee.LastName == "Doe")
matches.Add(employee);
}
Just use LINQ
var employee = Employees.Where(e => e.LastName == lastname).FirstOrDefault();
This will give you a IEnumerable<Employee> of employees with matched first and last name:
string firstName = "Bob";
string LastName = "Smith";
var employees = Employees.Where(e => e.FirstName == firstName &&
e.LastName == lastName);
Or with query syntax:
var employees = from e in Employees
where e.FirstName == firstName &&
e.LastName == lastName
select e;
If there should be only one employee matching that criteria, then use SingleOrDefault:
Employee employee = Employees.SignleOrDefault(e => e.FirstName == firstName &&
e.LastName == lastName);
When you say "find by his/her name" do you mean that you're looking for all of the people named "John" for example? Using LINQ, that would look like this:
var johns = Employees.Where(e => e.FirstName == "John");
Keep in mind that this returns an IEnumerable<Employee> instead of an IList<Employee>. There are a number of differences between the two.
.Where() is one of many extension methods that you can use to query and manipulate your lists.
You can use a loop, or use a LINQ:
This expression gives you the first employee with the last name "Smith", or null if nobody by this last name is found on your list:
var res = Employees.FirstOrDefault(e => e.LastName == "Smith");
This loop lets you enumerate all employees with the first name "John":
foreach (var employee in Employees.Where(e => e.FirstName == "John")) {
...
}
You can also make a list of employees passing a given filter:
var smiths = Employees.Where(e => e.LastName == "Smith").ToList();
If the original list contains no "Smith"s, the resulting list would be empty.
If you must not use LINQ, you can use a plain foreach loop:
foreach (var employee in Employees) {
if (employee.LastName == "Smith") {
...
}
}
http://code.msdn.microsoft.com/101-LINQ-Samples-3fb9811b
Check out the example for WHERE simple1 and simple2.
The best way to do this is using LINQ:
var employees = Employees.Where(e => e.FirstName.Equals("RuneS")).ToArray();
Try to use next code snippet to
var firstName = "Jon";
var lastName = "Skeet";
var employees = Employees.Where(emp => emp.FirstName == firstName && emp.LastName == lastName);
If you want to find exactly one employee, then you probably want to use Single instead of Where, which in my case is more plausible. In case if you want to find the first employee, that fits that criteria, use 'First` instead of 'Where'.
If you need to wild card search use contains.
Employees.Where(emp => emp.FirstName.Contains("Rune") && emp.LastName.Contains("S"));

find property value from List<Postavke>() with linq

I have class "Postavke" and then i store it into
List<Postavke> postavke = new List<Postavke>();
Now i want to find some elemnt (property) from this List. I know "Name", "Surname" and i want to get "Address".
How to get "Adress" if i know "Name" and "Surname". All this are properties in "Postavke" class
whole class
public class Postavke
{
#region Properties
public string Name { get; set; }
public string Surname { get; set; }
public string Address { get; set; }
#endregion
#region Methods
public Postavke(string name, string surname, string address, string oznakaLokacije, string oznakaZapore)
{
Name = ean;
Surname = surname;
Address = address;
}
#endregion
}
You can query postavke for all results that contain the name and surname and put the results into a list. Putting the results into a list I find makes things easier to validate and handle unforseen items as from the looks of it it is possible that duplicate items could appear.
if the results must have all data within the list item then:
List<Postavke> results = new List<Postavke>();
var query1 = from a in postavke
where a.Name == searchName
&& a.Surname == searchSurname
select a;
results.AddRange(query1);
this list will have all the results that contain the exact name and surname.
If you just want the address then you can use:
List<string> results = new List<string>();
var query1 = from a in postavke
where a.Name == searchName
&& a.Surname == searchSurname
select a.Address;
results.AddRange(query1);
this will produce a list of addresses. From here you can then validate the list if you want by doing such things as checking to see how many items in the list there are so you know how you want to handle it etc.
If you want to use just either the name or the surname then you can remove the line that asks for one or the other.
If you end up with duplicates but the results are the same then you can change the line
results.AddRange(query1);
to
results.AddRange(query1.Distinct());
by using the Distinct() method it will sort the query and remove duplicates so the list will not be in the same order as it would if you didn't use it.
If you only wanted one result then it's worth validating it by checking how many items are in the list by using
results.Count
you could if it equals 1 carry on, otherwise throw up a message or some other way you may want to handle it. Other pieces of validation that are good is to set values ToLower() and Trim() during the search as to avoid actually changing the original text.
So taking the last query as an example:
List<string> results = new List<string>();
var query1 = from a in postavke
where a.Name.ToLower().Trim() == searchName.ToLower().Trim()
&& a.Surname.ToLower().Trim() == searchSurname.ToLower().Trim()
select a.Address;
results.AddRange(query1);
you can also filter the query with the where clause after you make the query by doing:
List<string> results = new List<string>();
var query1 = from a in postavke
select a.Address;
query1 = query1.Where(h => h.Name == searchName);
query1 = query1.Where(h => h.Surname == searchSurname);
results.AddRange(query1);
The 2 where clauses were split to show you can perform where clause at different points so you could have where clauses within if statements. you can combine the 2 into:
query1 = query1.Where(h => h.Name == searchName && h.Surname == searchSurname);
Hopefully this helps
This will work if you can be sure there's exactly one match
var address = poatavke.Where(p=>p.Name == name && p.Surname == surname).Single().Address;
If you don't know if there's no matches or exactly one you can do:
var posta = poatavke.Where(p=>p.Name == name && p.Surname == surname).SingleOrDefault()
var address = posta == null ? string.Empty : posta.Address;
if you don't know how many matches there's going to be but always want the first (or are using ET which doesn't understand Single())
var posta = poatavke.Where(p=>p.Name == name && p.Surname == surname).FirstOrDefault()
var address = posta == null ? string.Empty : posta.Address;

Categories

Resources