Reputation: 413
I have a web application, made using c#/mvc/ef/unitofworkPattern. I have database with two tables, PERSON and CASENOTE. A PERSON may have multiple CASENOTES, but a CASENOTE may only have one PERSON. The key in PERSON is PasNo. This is a foreign key in CASENOTE, but it's not defined as a foreign key in DDL for reasons.
So, I have a search page, where someone can input data into a CASENOTE number textbox, and get back all people linked to any casenotes it may find. So they could put in just a partial casenote, and it would get back all casenotes containing that partial string, and then find all of the PEOPLE that have those casenotes using the PasNo in each casenote.
So here is my code:
public IEnumerable<PERSON> GetSearchResults(SearchViewModel viewModel)
{
var people = unitOfWork.PersonRepository.GetAll();
if (viewModel.Casenote != null)
{
var casenotes = unitOfWork.CasenoteRepository.GetAll()
.Where(x => x.CASENOTE1.Trim()
.ToLower()
.Contains(viewModel.Casenote.Trim().ToLower()))
.Take(1000)
.ToList();
if (casenotes.Count > 0)
{
foreach (var casenote in casenotes)
{
var pasint = casenote.PAS_INT_NO;
people = people.Where(w => w.PAS_INT_NO == pasint);
//it gives me the stackoverflow.exception at execution of the above line
}
}
}
return people.Take(1000).ToList().OrderBy(x => x.SURNAME).ThenBy(x => x.FORENAMES);
}
Can anybody help me and tell me what I'm doing wrong? I can provide more info if needed.
EDIT! I forgot to mention, when I put in a single letter into the CASENOTE textbox, I get the stackoverflow exception, but when I type more than 1 letter, I don't and the search works.
Upvotes: 0
Views: 106
Reputation: 8962
If you need to select from people
using or
condition over w.PAS_INT_NO == pasint
, this would work:
public IEnumerable<PERSON> GetSearchResults(SearchViewModel viewModel)
{
var people = unitOfWork.PersonRepository.GetAll();
if (viewModel.Casenote != null)
{
var casenoteNos = unitOfWork.CasenoteRepository.GetAll()
.Where(x => x.CASENOTE1.Trim()
.ToLower()
.Contains(viewModel.Casenote.Trim().ToLower()))
.Select(casenote => casenote.PAS_INT_NO)
.Take(1000)
.ToList();
if (casenoteNos.Any())
{
people = people.Where(w => casenoteNos.Contains(w.PAS_INT_NO));
}
}
return people.Take(1000).ToList().OrderBy(x => x.SURNAME).ThenBy(x => x.FORENAMES);
}
Description
The culprit in the original code was:
people = people.Where(w => w.PAS_INT_NO == pasint)
because IEnumerable
produced by Where
is not materialized, here we creating up to 1000 chained Where
, so eventually people
becomes people.Where(...).Where(...).Where...1000 times
.
If check the sources of Where
extension here, first Where
produces WhereEnumerableIterator
object then its Where
and consequent Where
s generate nested lambda calls here by calling CombinePredicates, which is:
static Func<TSource, bool> CombinePredicates<TSource>(Func<TSource, bool> predicate1, Func<TSource, bool> predicate2) {
return x => predicate1(x) && predicate2(x);
}
Which being called 1000 times becomes:
(x=> ...1000 times... (x => predicate1(x) && predicate2(x)) && predicate2(x)) ...1000 times... ) && predicate2(x)
Then 1000 nested calls produced a stack overflow exception.
Conclusion: don't chain a ton of Where
, it was not designed for that. Anyway chained Where
(a ton of AND
filters) here is not what was intended, but IN
/ Contains
is needed instead.
Upvotes: 2
Reputation: 580
Is there a reason you are getting a list of all the people at the top of the function? I'm not that familiar with EF so this syntax could be wrong, but I'm assuming that this would be much more efficient than getting every person in your database and then filtering that list:
foreach (var casenote in casenotes)
{
var pasint = casenote.PAS_INT_NO;
people = unitOfWork.PersonRepository.Where(w => w.PAS_INT_NO == pasint);
//it gives me the stackoverflow.exception at execution of the above line
}
Not sure if this will solve the problem, but if there are a lot of people and a lot of casenotes in your database that could definitely cause a SO exception.
Upvotes: 0