Wildcard search in using Lambda in EF - c#

I have a search with optional arguments:
string id1 = HttpContext.Current.Request["id1"];
string id2 = HttpContext.Current.Request["id2"];
List<Foo> list = context.Foo.Where(l =>
(string.IsNullOrEmpty(id1) || l.Id1.Contains(id1)) &&
(string.IsNullOrEmpty(id2) || l.Id2.Contains(id2)))
.Take(10)
.ToList());
I would like to extend it so that if the string starts with a *, the EndsWith() - method should be used.
For example, if the first searchstring is *123, I want to do a l.Id1.EndsWith("123").
Is there any way to extend my current code, or should I use a different approach?

I'm quite sure if this is what you were intending, but you can break your logic up by using IQueryable(T). The query won't be executed until you try to use the collection.
public IList<Foo> Search(DbContext<Foo> context, string id1, string id2)
{
Func<Foo, bool> predicate = l =>
(string.IsNullOrEmpty(id1) || l.Id1.Contains(id1))
&& (string.IsNullOrEmpty(id2) || l.Id2.Contains(id2)));
IQueryable<Foo> list = context.Foo.Where(predicate);
if(id1.StartsWith("*") && id1.Length > 1)
{
var searchTerm = id1.Substring(1, id1.Length);
list = list.Where(l => l.EndsWith(searchTerm));
}
return list.Take(10).ToList(); // Execution occurs at this point
}
Building up the query:
public void BasicSearch(IQueryable<foo> list, string id1, string id2)
{
Func<Foo, bool> predicate = l =>
(string.IsNullOrEmpty(id1) || l.Id1.Contains(id1))
&& (string.IsNullOrEmpty(id2) || l.Id2.Contains(id2)));
list.Where(predicate);
}
public void WildcardSearch(IQueryable<Foo> list, string id1)
{
if(!id1.StartsWith("*") || id1.Length <= 1) return;
var searchTerm = id1.Substring(1, id1.Length);
list.Where(l => l.EndsWith(searchTerm));
}
IQueryable<Foo> list = context.Foo;
BasicSearch(list, id1, id2);
WildcardSearch(list, id1);
var result = list.Take(10); // Execution occurs at this point

Related

C# linq query - Where with multiple ANDs

I am trying to query using EF. The user can use up to 3 search words but they are not required. How do I write an EF query that will work as an AND for all of the search words that are used but be able to remove an AND for any search word that is empty?
Example, I want the following to return the first two elements in the array for s1='mobile', s2='', and s3='laptop'. It's not returning any. It should return the first two if s2 is changed to s2='burke'.
Example:
using System;
using System.Linq;
public class Simple {
public static void Main() {
string[] names = { "Burke laptop mobile", "laptop burke mobile", "Computer Laptop",
"Mobile", "Ahemed", "Sania",
"Kungada", "David","United","Sinshia" };
//search words
string s1 = "mobile";
string s2 = "";
string s3 = "laptop";
var query = from s in names
where (!string.IsNullOrEmpty(s1) && s.ToLower().Contains(s1))
&& (!string.IsNullOrEmpty(s2) && s.ToLower().Contains(s2))
&& (!string.IsNullOrEmpty(s3) && s.ToLower().Contains(s3))
orderby s
select s.ToUpper();
foreach (string item in query)
Console.WriteLine(item);
}
}
Instead of trying to write a single where statement, you can optionally extend an existing IQueryable<T> (or IEnumerable<T> like our code sample would be using) by chaining Where statements.
var query = names;
if (!string.IsNullOrEmpty(s1))
{
query = query.Where(x => x.Contains(s1));
}
if (!string.IsNullOrEmpty(s2))
{
query = query.Where(x => x.Contains(s2));
}
// ... (or make a foreach loop if s1,s2,s3 were an array.
var results = query.OrderBy(x => x).Select(x => x.ToUpper());
Chaining Where like this is equivalent to "anding" all where predicates together.
EDIT:
To update why your specific implementation doesn't work is because your && operators are incorrect for the given use-case.
(string.IsNullOrEmpty(s1) || s.ToLower().Contains(s1)) &&
(string.IsNullOrEmpty(s2) || s.ToLower().Contains(s2)) &&
(string.IsNullOrEmpty(s3) || s.ToLower().Contains(s3))
Remember that && requires both left and right statements to be true, so in your case !string.IsNullOrEmpty(s2) && s.ToLower().Contains(s2) this is saying that s2 must always be not-empty/null.
Please consider this:
using System;
using System.Linq;
public class Simple {
public static void Main() {
string[] names = { "Burke laptop mobile", "laptop burke mobile", "Computer Laptop",
"Mobile", "Ahemed", "Sania",
"Kungada", "David","United","Sinshia" };
//search words
string s1 = "mobile";
string s2 = "";
string s3 = "laptop";
var query = from s in names
where (s1 != null && s.ToLower().Contains(s1))
&& (s2 != null && s.ToLower().Contains(s2))
&& (s3 != null && s.ToLower().Contains(s3))
orderby s
select s.ToUpper();
foreach (string item in query)
Console.WriteLine(item);
}
}
If you use an array or list instead of multiple strings, you could do something like
List<string> searchWords = new List<string>
{
"mobile",
"",
"laptop"
};
var query = names
.Where(n => searchWords
.Where(s => !string.IsNullOrEmpty(s))
.All(s => n.ToLower().Contains(s)))
.Select(n => n.ToUpper())
.OrderBy(n => n);
This also is more flexible, as you can have any number of search words, without changing the query.

How can I store linq queries?

I've researched but I haven't found anything about this situation.
I have two linq queries:
//checking all columns if there is
mockDataList = mockDataList.Where(w =>
w.Email.ToLower().Contains(search) ||
w.Gender.ToLower().Contains(search) ||
w.Name.ToLower().Contains(search) ||
w.Surname.ToLower().Contains(search) ||
w.Id.ToString().ToLower().Contains(search)
).Skip(start).Take(length).ToList();
//getting count info
var filteredTotal = mockDataList.Where(w =>
w.Email.ToLower().Contains(search) ||
w.Gender.ToLower().Contains(search) ||
w.Name.ToLower().Contains(search) ||
w.Surname.ToLower().Contains(search) ||
w.Id.ToString().ToLower().Contains(search)
).Count();
I want to count before to take 10 of them. Therefore I've had to write two queries. I don't want it to duplicate. How can I store without executing query?
*sorry for my grammar mistakes
LINQ queries are executed, when your loop through them via foreach or when you call methods like FirstOrDefault(), ToList(), ToArray(), ... So the following is no problem:
var query = mockDataList.Where(w =>
w.Email.ToLower().Contains(search) ||
w.Gender.ToLower().Contains(search) ||
w.Name.ToLower().Contains(search) ||
w.Surname.ToLower().Contains(search) ||
w.Id.ToString().ToLower().Contains(search)
); // nothing is done here, no filtering
mockDataList = query.Skip(start).Take(length).ToList(); // here, the filtering is done
var filteredTotal = query.Count(); // here, the filtering is done again
Just store LINQ query in the local variable:
var query = mockDataList.Where(w =>
w.Email.ToLower().Contains(search) ||
w.Gender.ToLower().Contains(search) ||
w.Name.ToLower().Contains(search) ||
w.Surname.ToLower().Contains(search) ||
w.Id.ToString().ToLower().Contains(search));
var filteredTotal = query.Count();
mockDataList = query.Skip(start).Take(length).ToList();
I think the better practice would be to store the IEnumerable as suggested by #SomeBody.
IEnumerable<MyClass> query = mockDataList.Where(w => .....);
List<MyClass> PaginatedFilteredItems = query.Skip(start).Take(length).ToList();
int FilteredItemsTotal = query.Count();
// Or
int PaginatedFilteredItemsTotal = PaginatedFilteredItems.Count;
Another approach whould be to use a Func or Expression to store your query:
public class MyClass{
public int Id;
public string Email;
public string Gender;
public string Name;
public string Surname;
}
Func<MyClass, bool> MyClassFunc = w =>
w.Email.ToLower().Contains(search) ||
w.Gender.ToLower().Contains(search) ||
w.Name.ToLower().Contains(search) ||
w.Surname.ToLower().Contains(search) ||
w.Id.ToString().ToLower().Contains(search);
then you could use it like this:
mockDataList = mockDataList.Where(MyClassFunc).Skip(start).Take(length).ToList();

How To: Use LINQ to search data for a string containing many possible types of "Single Quote"

The Problem:
My customers want to be able to search for first and last names in my site, using any version of ', `, or those goofy microsoft 'start' and 'end' quotes.
I would like to be able to write a single statement that looks for all possible permutations of that evil symbol, like such:
var customers = _sms.CurrentSession.Query<customer>()
.Where(c => c.FirstName.Contains(firstName)
&& c.LastName.Contains(lastName)
&& !c.IsEmployee)
.ToList();
and if the user was searching for "O'Brien", I would show them results for "O'Brien", and "O`Brien", and any other version of that stupid thing.
What's the best way to do that? Is there a way to do it (without a massive recursion where I do things like
if (firstName.Contains("'")
{
var customers = _sms.CurrentSession.Query<customer>()
.Where(c => c.FirstName.Contains(firstName)
&& c.LastName.Contains(lastName)
&& !c.IsEmployee)
.ToList();
firstName = firstName.Replace("'", "`");
customers.AddRange(_sms.CurrentSession.Query<customer>()
.Where(c => c.FirstName.Contains(firstName)
&& c.LastName.Contains(lastName)
&& !c.IsEmployee)
.ToList());
}
etc...
Update
private static readonly string[] Quotes = { "#", "'", "‘", "’", "`" };
private IEnumerable<customer> CustomersByFirstName(string firstName, bool showInactive = true)
{
firstName = firstName.Replace(Quotes[1], Quotes[0]);
var customers = _sms.CurrentSession.Query<customer>()
.Where(c => (c.FirstName.Replace(Quotes[1], Quotes[0]).Contains(firstName) ||
c.FirstName.Replace(Quotes[2], Quotes[0]).Contains(firstName) ||
c.FirstName.Replace(Quotes[3], Quotes[0]).Contains(firstName) ||
c.FirstName.Replace(Quotes[4], Quotes[0]).Contains(firstName))
&& !c.IsEmployee)
.ToList();
if (!showInactive)
{
customers = customers.Where(c => !c.Disabled).ToList();
}
return customers;
}
This is almost it... Almost
Final Update
This turns out to be exactly the solution, tho not as elegant and small as I might like:
private IEnumerable<customer> CustomersByFirstName(string firstName, bool showInactive = true)
{
firstName = firstName.Replace(Quotes[1], Quotes[0]);
firstName = firstName.Replace(Quotes[2], Quotes[0]);
firstName = firstName.Replace(Quotes[3], Quotes[0]);
firstName = firstName.Replace(Quotes[4], Quotes[0]);
var customers = _sms.CurrentSession.Query<customer>()
.Where(c => (c.FirstName.Replace(Quotes[1], Quotes[0]).Contains(firstName) ||
c.FirstName.Replace(Quotes[2], Quotes[0]).Contains(firstName) ||
c.FirstName.Replace(Quotes[3], Quotes[0]).Contains(firstName) ||
c.FirstName.Replace(Quotes[4], Quotes[0]).Contains(firstName))
&& !c.IsEmployee)
.ToList();
if (!showInactive)
{
customers = customers.Where(c => !c.Disabled).ToList();
}
return customers;
}
Sorry, I posted an answer without fully understanding the question. Here's the corrected answer.
I would define your criteria for a match in a separate function. Here's how I implemented it:
// Add quote characters as needed.
List<char> quotes = new List<char> { '\'', '‘', '’' };
public string CleanName(string name)
{
StringBuilder cleanName = new StringBuilder(name);
foreach (char quote in quotes)
{
cleanName = cleanName.Replace(quote, '\'');
}
return cleanName.ToString();
}
public bool IsMatch(string n0, string n1)
{
return String.Compare(CleanName(n0), CleanName(n1)) == 0;
}
Then, search for matching names using the IsMatch function, as demonstrated in this unit test:
[TestMethod]
public void TestQuoteSearch()
{
string searchName = "O'Brien";
var matchingNames = names.Where(name => IsMatch(name, searchName));
Assert.AreEqual(4, matchingNames.Count());
Assert.IsTrue(matchingNames.Contains("O'Brien"));
Assert.IsTrue(matchingNames.Contains("O‘Brien"));
Assert.IsTrue(matchingNames.Contains("O’Brien"));
Assert.IsFalse(matchingNames.Contains("Jones"));
}
You could do something like :
char[] all_quotes= new char[] { '\'', '‘', '’' /* any other quote you like */ };
Then replace them in your names with a special char (lets say *), or cut them out all together, and then search for those matches then.
have a look at this answer for an example of how to do it, or brew your own:
var customers = _sms.CurrentSession.Query<customer>()
.Where(c => c.FirstName.Replace(all_quotes, "*")
.Contains(firstName.Replace(all_quotes, "*"))
&& // same for others)
.ToList();
Regarding Final Update:
I wouldn't have the one to replace as part as the quotes. I'd do something like :
string inplace = "#";
private static readonly string[] Quotes = {"'", "‘", "’", "`" };
foreach (var q in Quotes)
firstName = firstName.Replace(q, inplace);
you can wrap the foreach in { } if you'd like. It's shorter, and if you end up adding a quote to the array, you don't need another line of "replace"
Create Store Procedure and import to Entity Modal then call like method.

Expression predicate must be built separately with if and else blocks?

I have 2 versions of codes for class method "GetUserRoles". Version 1 always works OK while Version 2 is not working if passed-in argument "exludeRoleNames" for that method is absent and exception "Null object reference" is thrown as a result. I would like to understand why Version 2 codes are not always working, and why Version 1 codes require building predicate with "if" and "else" blocks. Thank you in advance.
Version 1:
The following codes are always working. You could notice that there are separate "if" and "else" blocks to build local variable Expression<Func<UserRoleInApplication, bool>> predicate
public List<UserInRoleViewModel> GetUserRoles(long userId, params string[] exludeRoleNames)
{
List<UserInRoleViewModel> results = null;
IQueryable<UserInRoleViewModel> items = null;
Expression<Func<UserRoleInApplication, bool>> predicate = null;
if (exludeRoleNames.Count() <= 0)
{
predicate = x => true;
}
else
{
predicate = x => !exludeRoleNames.Contains(x.UserRole.RoleName);
}
items = from uir in _repository.GetQuery<UserInRole>(x => x.UserId == userId)
join ura in _repository.GetQuery<UserRoleInApplication>(predicate)
on uir.UserRoleInApplicationId equals ura.UserRoleInApplicationId
into g
from item in g
select new UserInRoleViewModel
{
UserInRoleId = uir.UserInRoleId,
UserId = uir.UserId,
UserRoleInApplicationId = uir.UserRoleInApplicationId
};
if (items != null && items.Any())
{
results = new List<UserInRoleViewModel>();
results = items.ToList();
}
return results;
}
Version 2:
However, the following codes throw exception "Null object reference" when calling method GetUserRoles such as GetUserRoles(long userId) without passed-in argument "exludeRoleNames". You could notice that there are no separate "if" and "else" blocks for the codes to form local variable Expression<Func<UserRoleInApplication, bool>> predicate:
public List<UserInRoleViewModel> GetUserRoles(long userId, params string[] exludeRoleNames)
{
List<UserInRoleViewModel> results = null;
IQueryable<UserInRoleViewModel> items = null;
// Note: one line code and no separate "if" and "else" blocks *************
Expression<Func<UserRoleInApplication, bool>> predicate = x => exludeRoleNames.Count() <= 0 ? true : !exludeRoleNames.Contains(x.UserRole.RoleName);
items = from uir in _repository.GetQuery<UserInRole>(x => x.UserId == userId)
join ura in _repository.GetQuery<UserRoleInApplication>(predicate)
on uir.UserRoleInApplicationId equals ura.UserRoleInApplicationId
into g
from item in g
select new UserInRoleViewModel
{
UserInRoleId = uir.UserInRoleId,
UserId = uir.UserId,
UserRoleInApplicationId = uir.UserRoleInApplicationId
};
if (items != null && items.Any()) // Note: throw exception "Null object reference" if parameter "exludeRoleNames" is absent on calling method GetUserRoles such as GetUserRoles(long userId);
{
results = new List<UserInRoleViewModel>();
results = items.ToList();
}
return results;
}
Try changing this:
Expression<Func<UserRoleInApplication, bool>> predicate = x => exludeRoleNames.Count() <= 0 ? true : !exludeRoleNames.Contains(x.UserRole.RoleName);
To this:
Expression<Func<UserRoleInApplication, bool>> predicate = x => true;
if(exludeRoleNames != null)
{
foreach(string exl in exludeRoleNames)
{
string temp = exl;
predicate = predicate.Or(x=>x.UserRole.RoleName == temp);
}
}
The problem is that you're trying to call Count() on exludeRoleNames - which is null. So, rather than check Count(), compare it to null. If it is null, then you can treat it as an empty array. If it isn't null, then check it's contents.
The other problem is that you can't use string[].Contains in a query context. Entity Framework (which I assume you are using) doesn't support that. So, you have to build out the predicate.
I re-write the following codes and they are working. In case of absent parameter "exludeRoleNames", the codes creat an empty string array:
public List<UserInRoleViewModel> GetUserRoles(long userId, params string[] exludeRoleNames)
{
List<UserInRoleViewModel> results = null;
IQueryable<UserInRoleViewModel> items = null;
exludeRoleNames = !exludeRoleNames.Any() ? new string[] { } : exludeRoleNames; // a MUST
Expression<Func<UserRoleInApplication, bool>> predicate = x => !exludeRoleNames.Contains(x.UserRole.RoleName);
items = from uir in _repository.GetQuery<UserInRole>(x => x.UserId == userId)
join ura in _repository.GetQuery<UserRoleInApplication>(predicate)
on uir.UserRoleInApplicationId equals ura.UserRoleInApplicationId
into g
from item in g
select new UserInRoleViewModel
{
UserInRoleId = uir.UserInRoleId,
UserId = uir.UserId,
UserRoleInApplicationId = uir.UserRoleInApplicationId
};
if (items != null && items.Any())
{
results = new List<UserInRoleViewModel>();
results = items.ToList();
}
return results;
}

dynamic orderBy using linq dynamic

I am trying to convert this Func to use string values using linq.dynamic.
Currently I have
Func<IQueryable<Customer>, IOrderedQueryable<Customer>> orderBy = o => o.OrderBy(c => c.Postcode);
But I want to do
string sortItem = "customer";
string order = "ASC"
Func<IQueryable<Customer>, IOrderedQueryable<Customer>> orderBy = o => o.OrderBy(sortItem + " " + order);
I am using the Linq.Dynamic library but I am unable to get it to work with the function.
Any help...
Like the other answer suggests, this may not be possible. However, I wanted to post some code which I wrote recently to do something similar:
// single column sorting support
var sortColumnIndex = Convert.ToInt32(Request["iSortCol_0"]);
Func<LegalComplianceDatatable, string> orderingFunction = (c => sortColumnIndex == 0 ? c.HomeCountry :
sortColumnIndex == 1 ? c.HostCountry :
sortColumnIndex == 2 ? c.YearOneRate :
sortColumnIndex == 3 ? c.YearOtherRate :
sortColumnIndex == 4 ? c.RateType :
c.HostCountry);
if (Request["sSortDir_0"] == "desc")
{
filteredResults = filteredResults.OrderByDescending(orderingFunction);
}
else
{
filteredResults = filteredResults.OrderBy(orderingFunction);
}
I haven't used Linq.Dynamic but you can achieve this if you are comfortable building your own expression tree. For example:
public static class IQueryableExtension
{
public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> query, string propertyName)
{
var memberProp = typeof(T).GetProperty(propertyName);
var method = typeof(IQueryableExtension).GetMethod("OrderByInternal")
.MakeGenericMethod(typeof(T), memberProp.PropertyType);
return (IOrderedQueryable<T>)method.Invoke(null, new object[] { query, memberProp });
}
public static IOrderedQueryable<T> OrderByInternal<T, TProp>(IQueryable<T> query, PropertyInfo memberProperty)
{
if (memberProperty.PropertyType != typeof(TProp)) throw new Exception();
var thisArg = Expression.Parameter(typeof(T));
var lamba = Expression.Lambda<Func<T, TProp>>(Expression.Property(thisArg, memberProperty), thisArg);
return query.OrderBy(lamba);
}
}
And you can use this like:
IQueryable<Customer> query; // Some query
query = query.OrderBy("Name"); // Will return an IOrderedQueryable<Customer>
This is the logic for ascending sort, without checking (you will need to make sure the property exists, and so on) and some things can be optimized (the reflected method invocation for example). It should get you started.

Categories