I don't have a lot of experience with IQueryable
What I am trying to do is search for a user based on a list of passed in constraints that can either be a username, or phone number. Depending on the type I want to return limited information. I then want to combine the 3 IQueryables into one and combine entries with matching id and username to maintain the most information.
Here is what i have so far:
public IQueryable<User> Search(String[] criteria)
{
var query = Database.Get<User>();
IQueryable<User> phoneQuery = null;
IQueryable<User> emailQuery = null;
IQueryable<User> nameQuery = null;
foreach (String str in criteria)
{
// Check if it is a phone number
if (Regex.IsMatch(str, #"([0-9]{3})?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$"))
{
phoneQuery = query.Where(
u => u.PhoneNumber.ToLower() == (str))
.Select(i => new User
{
Id = i.Id,
UserName = i.Name,
PhoneNumber = i.PhoneNumber
})
}
// check if it is an email
else if (criteria.Contains("#"))
{
emailQuery = query.Where(
u => u.Email.ToLower() == (str))
.Select(i => new User
{
Id = i.Id,
UserName = i.Name,
Email = i.Email
})
}
else
{
nameQuery = query.Where(
u => u.UserName.ToLower() == (str))
.Select(i => new User
{
Id = i.Id,
UserName = i.Name,
})
}
}
// Merge the 3 queries combining entries if the username and id match and maintain the maximum amount of information
return query;
}
There are a few issues with your code:
ToList() will execute the query. If you call AsQueryable() later, you simply create an object query on the local objects. This basically loses the notion of IQueryable, so you'd better delete all ToList() and AsQueryable() calls.
You can make it a single query instead of merging the three queries, like so:
Expression predicateBody = Expression.Constant(false);
Expression userParameter = Expression.Parameter("user", typeof(User));
Expression userUserName = Expression.MakeMemberAccess(...);
Expression userPhoneNumber = Expression.Cal(...);
Expression userEmail = Expression.Call(...);
foreach (String str in criteria)
{
// Check if it is a phone number
if (Regex.IsMatch(str, #"([0-9]{3})?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$"))
{
predicateBody = Expression.Or(predicateBody, Expression.Equals(userPhoneNumber, Expression.Constant(str)));
}
// check if it is an email
else if (criteria.Contains("#"))
{
predicateBody = Expression.Or(predicateBody, Expression.Equals(userEmail, Expression.Constant(str)));
}
else
{
predicateBody = Expression.Or(predicateBody, Expression.Equals(userUserName, Expression.Constant(str)));
}
}
return query.Where(Expression.Lambda<Func<User, bool>>(predicateBody, userParameter))
.GroupBy(u => u.Id)
.Select(users => new User()
{
Id = users.Key,
UserName = users.Select(u => u.UserName).Intersect(criteria).FirstOrDefault(),
Email = users.Select(u => u.Email).Intersect(criteria).FirstOrDefault(),
PhoneNumber = users.Select(u => u.PhoneNumber).Intersect(criteria).FirstOrDefault()
});
Edit Sorry, I misunderstood the merging problem.
Edit2 If the criterias are sorted in advance, there is also a solution that does not require to manually creating the expression tree.
Edit3 I see, I forgot the part with the limited information.
var phoneNumbers = new List<string>();
var emails = new List<string>();
var userNames = new List<string>();
foreach (var str in criteria)
{
// Check if it is a phone number
if (Regex.IsMatch(str, #"([0-9]{3})?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$"))
{
phoneNumbers.Add(criteria);
}
// check if it is an email
else if (criteria.Contains("#"))
{
emails.Add(crietria);
}
else
{
userNames.Add(criteria);
}
}
return query.Where(u => phoneNumbers.Contains(u.PhoneNumber)
|| emails.Contains(u.Email)
|| userNames.Contains(u.UserName))
.GroupBy(u => u.Id)
.Select(users => new User()
{
Id = users.Key,
UserName = users.Select(u => u.UserName).Intersect(userNames).FirstOrDefault(),
Email = users.Select(u => u.Email).Intersect(emails).FirstOrDefault(),
PhoneNumber = users.Select(u => u.PhoneNumber).Intersect(phoneNumbers).FirstOrDefault()
});
This is what I ended up going with.
public IQueryable<User> Search(String[] criteria)
{
var query = Database.Get<User>();
List<User> res = new List<User>();
foreach (String str in criteria)
{
// Check if it is a phone number
if (Regex.IsMatch(str, #"([0-9]{3})?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$"))
{
var users = query.Where(
u => u.PhoneNumber.ToLower() == (str))
.Select(i => new User
{
Id = i.Id,
UserName = i.Name,
Email = null,
PhoneNumber = i.PhoneNumber
});
// Multiple users can have the same phone so add all results
foreach (User u in users)
{
if (u != null) { res.Add(u); }
}
}
// Check if it is an email match
else if (str.Contains("#"))
{
var user = query.Where(
u => u.Email.ToLower() == (str))
.Select(i => new User
{
Id = i.Id,
UserName = i.Name,
Email = i.Email,
PhoneNumber = null
}).SingleOrDefault<User>(); // Only one user can use an email
if (user != null) { res.Add(user); }
}
// Otherwise it is a username
// NOTE: If a username is all digits and dashes it won't be
// searched for because it is interpreted as a phone number!
else
{
var user = query.Where(
u => u.UserName.ToLower() == (str))
.Select(i => new User
{
Id = i.Id,
UserName = i.Name,
Email = null,
PhoneNumber = null
}).SingleOrDefault<User>(); // Only one user can use an email
if (user != null) { res.Add(user); }
}
}
query = res.AsQueryable();
// Group the results by username and id and return all information that was found
query = from u in query
group u by new
{
u.Id,
u.UserName
} into g
select new User()
{
Id = g.Key.Id,
UserName = g.Key.UserName,
Email = g.Select(m => m.Email).SkipWhile(string.IsNullOrEmpty).FirstOrDefault(),
PhoneNumber = g.Select(m => m.PhoneNumber).SkipWhile(string.IsNullOrEmpty).FirstOrDefault()
};
return query;
}
Related
I want to sort the data based on CreatedUtc time. I have tried to use Reverse function, it seems to work out but still looking for some alternate option.
var result = _participantRepo.AsQueryable().Where(x => x.Id == ParticipantId).SelectMany(x =>
x.Relations).ToList().Where(x => x.UserId != AppUserId).Select(r => new RelationVM
{
IsOwner = r.UserId == participant.CreatedByUserId,
FirstName = r.FirstName,
LastName = r.LastName,
Email = r.Email,
UserId = r.UserId,
RelationType = r.RelationType,
Role = r.Role,
IsAccepted = r.IsAccepted,
AvatarUrl = r.AvatarUrl,
CreatedUtc = r.CreatedUtc
}).Reverse().ToList();
There are 2 things you need to concern:
You can sort the elements of a sequence by using OrderBy
You should not .ToList() when you have not done, So you might to read LINQ deferred (or immediate?) execution to have a better understanding.
As a result, your query should look like this
var result = _participantRepo.AsQueryable().Where(x => x.Id == ParticipantId).SelectMany(x =>
x.Relations).Where(x => x.UserId != AppUserId).Select(r => new RelationVM
{
IsOwner = r.UserId == participant.CreatedByUserId,
FirstName = r.FirstName,
LastName = r.LastName,
Email = r.Email,
UserId = r.UserId,
RelationType = r.RelationType,
Role = r.Role,
IsAccepted = r.IsAccepted,
AvatarUrl = r.AvatarUrl,
CreatedUtc = r.CreatedUtc
}).Reverse().OrderBy(g => g.CreatedUtc).ToList();
How about .OrderBy(g => g.CreatedUtc) ?
I have function that searches for a list of users with their username, user address, previous address, and phone numbers.
Once I have those user information, I can filter against their username, address, previous address or phone number.
When I get the user result and filter for a specific phone number, the filter returns the correct phone number and other relevant information.
The problem arises when I search for a username that does not have a phone number. So when I search for a username and then filter for a specific username, the result is empty.
For example, If I search for username "John", I get 100 results matching that username along with their useraddress, previousaddress and phonenumber.
Filtering for John123 works without any issues.
However, If I search for "Mary", and try to filter against a specific username, I don't get any result back. This is because all usernames with "Mary" do not have a phonenumber.
The code below is what I've tried and it isn't working as expected. Could someone please take a look and tell me where I'm going wrong?
var finalResult = context.User
.Select(x => new UserModel()
{
UserName = x.Name,
UserAddress = x.Address,
PreviousAddress = x.PAddress,
PhoneNumber = x.PhoneNumbers.Select(y => y.PersonalNumber),
}).AsQueryable();
if (!string.IsNullOrWhiteSpace(search.UserName))
{
finalResult = finalResult.Where(x => EF.Functions.Like(x.UserName, $"%{search.UserName}%"));
if (search.UserNameFilter != "" || search.PhoneNumberFilter != "")
{
finalResult = finalResult.Where(x =>
((EF.Functions.Like(x.PhoneNumber.FirstOrDefault(), $"%{search.PhoneNumberFilter}%")) &&
EF.Functions.Like(x.UserName, $"%{search.UserName}%") &&
EF.Functions.Like(x.UserName, $"%{search.UserNameFilter}%") &&
EF.Functions.Like(x.PreviousAddress, $"%{search.PreviousAddressFilter}%") &&
EF.Functions.Like(x.UserAddress, $"%{search.UserAddressFilter}%")));
return finalResult;
}
return finalResult;
}
Split out the tests and guard each filter with an if separately:
var finalResult = context.User
.Select(x => new UserModel() {
UserName = x.Name,
UserAddress = x.Address,
PreviousAddress = x.PAddress,
PhoneNumber = x.PhoneNumbers.Select(y => y.PersonalNumber),
});
if (!String.IsNullOrWhiteSpace(search.UserName))
finalResult = finalResult.Where(x => EF.Functions.Like(x.UserName, $"%{search.UserName}%"));
if (search.UserNameFilter != "")
finalResult = finalResult.Where(x => EF.Functions.Like(x.UserName, $"%{search.UserNameFilter}%"));
if (search.PhoneNumberFilter != "")
finalResult = finalResult.Where(x => EF.Functions.Like(x.PhoneNumber.FirstOrDefault(), $"%{search.PhoneNumberFilter}%"));
if (search.PreviousAddressFilter != "")
finalResult = finalResult.Where(x => EF.Functions.Like(x.PreviousAddress, $"%{search.PreviousAddressFilter}%"));
if (search.UserAddressFilter != "")
finalResult = finalResult.Where(x => EF.Functions.Like(x.UserAddress, $"%{search.UserAddressFilter}%"));
return finalResult;
PS Don't call AsQueryable on a IQueryable - always know your types.
i am not before dev pc. i just started working with EF. so curious to know can we pass column name dynamically for where clause.
see a screen shot for searching grid.
i just compose a sample query. please tell me does it work?
public ActionResult Index(String ColumnName,String SearchText)
{
private CustomersEntities db = new CustomersEntities();
var customer = (from s in db.Customers
select new CustomerDTO
{
CustomerID = s.CustomerID,
CompanyName = s.CompanyName,
ContactName = s.ContactName,
ContactTitle = s.ContactTitle,
Address = s.Address
})
.Where(s => s.Field<string>(ColumnName).ToUpper().Contains(SearchText.ToUpper());
return View(customer);
}
thanks
public ActionResult Index(string ColumnName, string SearchText)
{
var arg = Expression.Parameter(typeof(Customer), "x");
var strType = typeof(string);
var ToUpperMeth = strType.GetMethods().Where(x => x.Name == nameof(string.ToUpper)
&& x.GetParameters().Count() == 0).Single();
var ContainsMeth = strType.GetMethods().Where(x => x.Name == nameof(string.Contains)
&& x.GetParameters().Count() == 1).Single();
var exprVal = Expression.Constant(SearchText);
var toUpExprVal = Expression.Call(exprVal, ToUpperMeth);
var exprProp = Expression.Property(arg, ColumnName);
var toUpExpr = Expression.Call(exprProp, ToUpperMeth);
var contExpr = Expression.Call(toUpExpr, ContainsMeth, toUpExprVal);
var predicate = Expression.Lambda<Func<Customer, bool>>(contExpr, arg);
var customer = (from s in db.Customers
select new CustomerDTO
{
CustomerID = s.CustomerID,
CompanyName = s.CompanyName,
ContactName = s.ContactName,
ContactTitle = s.ContactTitle,
Address = s.Address
}).Where(predicate).ToList();
return View(customer);
}
You can create something like this in repository (if you use it)
public IQueryable<T> FindBy(Expression<Func<T, bool>> predicate)
{
return _context.Set<CustomersEntities>().Where(predicate);
}
and then
var result = _repository.FindBy(y => y.CompanyName.IndexOf(SearchText, StringComparison.OrdinalIgnoreCase) >= 0);
The basic pattern is to build up the query at runtime, selectively adding Where expressions before running the query and projecting the results into the DTO.
Like this:
public IList<CustomerDTO> FindCustomers(String ColumnName, String SearchText)
{
var query = from s in db.Customers select s;
if (ColumnName == "CompanyName")
{
query = query.Where(c => c.CompanyName == SearchText);
}
else if (ColumnName == "ContactName")
{
query = query.Where(c => c.ContactName == SearchText);
}
//. . .
else
{
throw new InvalidOperationException($"Column {ColumnName} not found or not supported for searching.");
}
var results = from c in query
select new CustomerDTO()
{
CustomerID = c.CustomerID,
CompanyName = c.CompanyName,
ContactName = c.ContactName,
ContactTitle = c.ContactTitle,
Address = c.Address
};
return results;
}
Please look at my code below
var result = (from c in db.vCompanies
where id == c.id
from user in db.Users
select new ViewCompanyModel()
{
Id = c.id,
Descripton = c.Descripton,
Name = c.Name,
ImageLink = c.ImageLink,
AdminEmail = c.Email,
Users = db.eUsers
.Where(o => o.CompanyName.Equals(c.Name))
.Select(o => new UserManageViewModel.UserViewModel
{
Id = o.UserId,
Name = o.LastName,
Email = o.Email,
Company = o.CompanyName,
ListOfRoles = user.AspNetUsers.AspNetRoles.Select(x=>x.Name).ToList()
})
}).FirstOrDefault();
I receive not correct data in ListOfRoles - I receive data only of first user.
I tried to add something like this
Where(x=>x.UserId == o.UserId)
I also tried change for this
ListOfRoles = db.Users.Where(x=>x.UserId == o.UserId).Select(x=>x.AspNetUsers.AspNetRoles)
But in this case I can't select x.Name.
I am doing something wrong.
Please advise me.
if(result != null)
{
foreach (var user in result.Users)
{
var roles = db.Users.Where(x => x.UserId == user.Id).Select(x => x.AspNetUsers.AspNetRoles).FirstOrDefault();
user.ListOfRoles = roles.Select(x => x.Name).ToList();
}
}
This is the answer! It's work!
I have the following database code:
static IEnumerable<dynamic> GetData(bool withchildren) {
using (var model = new testEntities()) {
var res = default(IQueryable<dynamic>);
if (withchildren) {
res = model.UserSet
.Where(u => u.name != "")
.Select(u => new {
Name = u.name,
Email = u.email,
Groups = u.GroupSet.Select(g => new {
Name = g.name,
Id = g.Id
})
});
} else {
res = model.UserSet
.Where(u => u.name != "")
.Select(u => new {
Name = u.name,
Email = u.email
});
}
return res.ToList()
}
}
I would like to shrink the code and write it like this:
static IEnumerable<dynamic> GetData(bool withchildren) {
using (var model = new testEntities()) {
var res = default(IQueryable<dynamic>);
res = model.UserSet
.Where(u => u.name != "")
.Select(u => {
dynamic item = new {
Name = u.name,
Email = u.email
};
if(withchildren) {
item.Groups = u.GroupSet.Select(g => new {
Name = g.name,
Id = g.Id
});
}
return item;
});
return res.ToList();
}
}
But Visual Studio complains, that it cannot convert the lambda expression into an expression tree.
My question is, is there a way to accomplish that with the Entity Framework and Linq? I really wouldn't want to use ADO.net directly.
Maybe there is even a better version to shrink it, than the code that I imagine.
Here is a related question with Linq-To-Objects.
EDIT
Before someone asks, I use dynamic in the example code to make it a bit easier and faster.
EDIT 2
My goal with this approach is, to only query the fields I need to improve performance. Check http://www.progware.org/Blog/post/Slow-Performance-Is-it-the-Entity-Framework-or-you.aspx.
At the moment we use something like
static IEnumerable<dynamic> GetData(bool withchildren) {
using (var model = new testEntities()) {
var res = default(IQueryable<dynamic>);
res = model.UserSet
.Where(u => u.name != "")
.ToList();
return res;
}
}
And the performance is, according to Glimpse, horrible.
EDIT 3
Short side note, I made up some quick and dirty code. That is, why the foreach at the end was not needed. The actual code is not available at the moment.
Is there any reason you couldn't use:
res = model.UserSet
.Where(u => u.name != "")
.Select(u => new {
Name = u.name,
Email = u.email,
Groups = withchildren
? u.GroupSet.Select(g => new {
Name = g.name,
Id = g.Id
})
: null;
})
};
or perhaps:
res = model.UserSet
.Where(u => u.name != "")
.ToList() // ToList() here to enumerate the items
.Select(u => {
dynamic item = new {
Name = u.name,
Email = u.email
};
if(withchildren) {
item.Groups = u.GroupSet.Select(g => new {
Name = g.name,
Id = g.Id
});
}
return item;
});
One approach that would allow you to eliminate some code would be:
var res = model.UserSet.Where(u => u.name != "");
if (withchildren) {
res = res.Select(u => new {
Name = u.name,
Email = u.email,
Groups = u.GroupSet.Select(g => new {
Name = g.name,
Id = g.Id
})
});
} else {
res = res.Select(u => new {
Name = u.name,
Email = u.email
});
}
One of the most requested by community feature is support of multi-line expressions in EF,
however so far, you can use only conditional operator "?:" as well as wrap result in
one common type, so both your results will have to have "Groups" field.
Also there are an extensions to linq-providers such as https://www.nuget.org/packages/LinqKit/ ,
however they are based on own conventions so any developer should study it in depth before
taking advance in applying and supporting code written upon that extensions.