C# - Parsing Data inside Lambda Linq Block | Splitting .Where() - c#

I want to filter Entities from a Database, but I am kinda stuck on how to correctly chain the Methods together....
The User can search from one Textfield the Id, Title and Description. This SearchString will be bound to SearchString in SearchData.
I have a method:
public List<Movies> Search(SearchData search)
{
var movies = from m in entities select m;
if (!String.isNullOrEmpty(search.SearchString))
{
movies = movies.Where(x => x.Title.Contains(search.SearchString)).Where(//descritpion);
}
return movies;
}
This works, but I also need to check for the Id
.Where(x=>x.Id == search.SearchString)
This won´t work since Id is a int and SearchString a String.
I have tried multiple ways to do so:
I did use "Convert.ToInt32" on the SearchString or "Convert.ToString" on the Id, but out of some reason I won´t get anything back with this and an Error when I search for a String.
I tried to use a block with in the where : .Where(x => {if(Tryparse(Searchstring) {}else{}}), but it doesn´t like it when I try to return the Movie object or null.
I also tried to split the clause up:
if (int.tryparse(searchstring))
movies = movies.where(x=>x.id ==Int32.Parse(SearchString));
movies = movies.where(//title and desc)
,but with this all the Movies I have found in the if will be filtered out due to the title and desc.
My questions are:
1.)Is it possible to "split" those Methods so that they behave like an OR instead of an AND?.... Or that they will not be executed anymore after one worked since the User will only be allowed to enter an Int OR a String. I have more values I am filtering against for which I would need this too.
2)How can I test against more "complex" Logic inside the Lambdas?

I'm not sure if I understand it correctly but If you want to search where id == search.SearchString, and also any other condition with OR then you should do something like this:
.Where(x=>x.Id == search.SearchString && (x.description.Contains(search.Description) || x.Title.Contains(search.Title) || x.Whatever.Contains(search.Whatever)));

You can use an OR in the Where clause.
public List<Movies> Search(SearchData search)
{
var movies = from m in entities select m;
if (!String.IsNullOrEmpty(search.SearchString))
{
movies = movies.Where(x => x.Title.Contains(search.SearchString) || x.Description.Contains(search.SearchString));
}
return movies;
}

I don't understand why the movie Title should contain the movie Id, for my point of view it's bad practice,
i think you need to update the SearchData and add a separate field for the Id, and use it for filtering, this will make things more clear and easy for debugging

In order to respect both conditions, I'd first check whether the search string can be parsed to an integer. If so, you can compare the id against it, if not, the Id comparison should be omitted:
public List<Movies> Search(SearchData search)
{
var movies = from m in entities select m;
if (!String.isNullOrEmpty(search.SearchString))
{
int? searchId;
if (int.TryParse(search.SearchString, out i)
searchId = i;
else
searchId = null;
movies = movies.Where(x =>
(searchId.HasValue && x.Id == searchId.Value)
|| x.Title.Contains(search.SearchString)).Where(//descritpion);
}
return movies;
}
In the comparison, the first part checks whether the searchId is set and - if so - compares the id of the row against it. In addition, it checks whether the title contains the search string. If any of the two conditions are met, the row is returned. However, if the user enters 123 and this is both a valid id and part of a title, both rows will be returned.
If you want to search for id (exclusive) or a part of a text, you could use the following approach:
public List<Movies> Search(SearchData search)
{
var movies = from m in entities select m;
if (!String.isNullOrEmpty(search.SearchString))
{
Expression<Func<Movie, bool>> whereClause;
if (int.TryParse(search.SearchString, out searchId)
whereClause = (x) => x.Id == searchId;
else
whereClause = (x) => x.Title.Contains(search.SearchString);
movies = movies.Where(whereClause).Where(//descritpion);
}
return movies;
}
When entering 123, above sample searches for the id, when entering anything that cannot be parsed into an integer, it looks for a part of the title.
As for your second question: when using Entity Framework, the conditions in your lambda expressions are translated to SQL. This limits the available options. However, as shown in the first example, you can prepare your data and adjust the conditions accordingly in many cases. Nevertheless, not every valid lambda expression can be translated to SQL.

Related

Drop 'Where' and move the condition into the 'First' in LINQ

I have a windows application which has some similar code as below.
As class named Order.
class Order{
public string Orderid { get; set; }
public string CustName { get; set; }
}
Now, in another class in this application, object for Order class is created and value is assigned to it.
Order order = new Order();
order = JObject.Parse(some JSON data).ToObject<Order>();
Now I want to extract the CustName based on Orderid from order. For this I have used LINQ.
string custName = order.Where(s => s.Key == "o123").First().Value;
I'm using Sonarqube to check the code quality. When I run the SonarQube tool , it is showing that I need to refactor my code where I have used LINQ. This is the exact line it shows.
Drop 'Where' and move the condition into the 'First'.
I have searched for it a lot but couldn't understand what it is trying to say. Can anyone explain me how to refactor this line , so that it passes the SonarQube expectations.
Any input is highly helpful.Thanks.
You are perfoming an operation in two steps when you could do that by using the First lambda expression
string custName = order.First(s => s.Key == "o123").Value;
Linq method First definition:
First<TSource>(this IEnumerable<TSource>, Func<TSource, Boolean>)
First parameter is the IEnumerable you are using (Linq are extension methods)
Second parameter allows you to set the filter declaring a Func<TSource, Boolean> as parameter, that you could define as s => s.Key == "o123"
What it is telling you is that the Where is unnecessary, and the code can be expressed as this:
string custName = order.First(s => s.Key == "o123").Value;
The logic of the original code is this:
"look through the list to find any matches, and take the first"
Which is the same as the changed code:
"take the first match in the list"
Note though that this code will throw an exception if there is no match. If there's ever a possibility that there will not be a matching customer, use FirstOrDefault instead:
string custName;
var customer = order.FirstOrDefault(s => s.Key == "o123");
if (customer != null) {
custName = customer.Value;
}
else {
// code to handle no match
}

EF filtering/searching with multiple words

I have a simple custom table with a search/filter field. I leave the implementation of the search up to each use of the table.
So let's say I have users in my table and I want to search for them. I want to search both in users firstname, lastname and also any role they are in.
This would probably do the trick
searchString = searchString.ToLower();
query = query.Where(
x =>
x.FirstName.ToLower().Contains(searchString)
||
x.LastName.ToLower().Contains(searchString)
||
x.Roles.Any(
role =>
role.Name.ToLower().Contains(searchString)
)
);
But now I want to search/filter on multiple words. First I get an array of all separate words.
var searchStrings = searchString.ToLower().Split(null);
I tried the following but it does not fulfill my requirements listed further down as it returns any user where any word is matched in any field. I need that all words are matched (but possibly in different fields). Se below for more details.
query = query.Where(
x =>
searchStrings.Any(word => x.FirstName.ToLower().Contains(word))
||
searchStrings.Any(word => x.LastName.ToLower().Contains(word))
//snipped away roles search for brevity
);
First let me produce some data
Users (data)
Billy-James Carter is admin and manager
James Carter is manager
Billy Carter has no role
Cases
If my search string is "billy car" I want Billy-James and Billy returned but not James Carter (so all words must match but not on same field).
If my search string is "bil jam" or even "bil jam car" I only want Billy-James returned as he is the only one matching all terms/words. So in this the words bil and jam were both found in the FirstName field while the car term was found in the LastName field. Only getting the "car" part correct is not enough and James is not returned.
If I search for "car man" Billy-James and James are both managers (man) and named Carter and should show up. should I search for "car man admi" then only Billy-James should show up.
I am happy to abandon my current approach if better is suggested.
I cannot think of a way to wrap what you're looking for up into a single LINQ statement. There may be a way, but I know with EF the options are more limited than LINQ on an object collection. With that said, why not grab a result set from the database with the first word in the split, then filter the resulting collection further?
var searchWords = searchString.ToLower().split(' ');
var results = dataBase.Where(i => i.FirstName.ToLower().Contains(searchWords[0])
|| i.LastName.ToLower().Contains(searchWords[0])
|| i.Role.ToLower().Contains(searchWords[0]));
if(searchWords.Length > 1) {
for(int x = 1; x < searchWords.Length; x++) {
results = results.Where(i => i.FirstName.ToLower().Contains(searchWords[x])
|| i.LastName.ToLower().Contains(searchWords[x])
|| i.Role.ToLower().Contains(searchWords[x]));
}
}
Your final content of the results collection will be what you're looking for.
Disclaimer: I didn't have a setup at the ready to test this, so there may be something like a .ToList() needed to make this work, but it's basically functional.
Update: More information about EF and deferred execution, and string collection search
Given we have the schema:
Employee:
FirstName - String
Last Name - String
Roles - One to Many
Role:
Name - String
The following will build a query for everything you want to find
var searchTerms = SearchString.ToLower().Split(null);
var term = searchTerms[0];
var results = from e in entities.Employees
where (e.FirstName.Contains(term)
|| e.LastName.Contains(term)
|| e.Roles.Select(r => r.Name).Any(n => n.Contains(term)))
select e;
if (searchTerms.Length > 1)
{
for (int i = 1; i < searchTerms.Length; i++)
{
var tempTerm = searchTerms[i];
results = from e in results
where (e.FirstName.Contains(tempTerm)
|| e.LastName.Contains(tempTerm)
|| e.Roles.Select(r => r.Name).Any(n => n.Contains(tempTerm)))
select e;
}
}
At this point the query still has not been executed. As you filter the result set in the loop, this is actually adding additional AND clauses to the search criteria. The query doesn't execute until you run a command that does something with the result set like ToList(), iterating over the collection, etc. Put a break point after everything that builds the query and take a look at it. LINQ to SQL is both interesting and powerful.
More on deferred execution
The one thing which needs explanation is the variable tempTerm. We need a variable which is scoped within the loop so that we don't end up with one value for all the parameters in the query referencing the variable term.
I simplified it a bit
//we want to search/filter
if (!string.IsNullOrEmpty(request.SearchText))
{
var searchTerms = request.SearchText.ToLower().Split(null);
foreach (var term in searchTerms)
{
string tmpTerm = term;
query = query.Where(
x =>
x.Name.ToLower().Contains(tmpTerm)
);
}
}
I build a much bigger query where searching is just a part, starting like this
var query = _context.RentSpaces.Where(x => x.Property.PropertyId == request.PropertyId).AsQueryable();
above search only uses one field but should work just fine with more complex fields. like in my user example.
I usually take the apporach to sort of queue the queries. They are all executed in one step at the database if you look with the diagnostic tools:
IQueryable<YourEntity> entityQuery = context.YourEntity.AsQueryable();
foreach (string term in serchTerms)
{
entityQuery = entityQuery.Where(a => a.YourProperty.Contains(term));
}

Poorly performing query needs rewriting

I have a piece of code that's performing badly, and need to rewite it to introduce a proper where clause before starting the .ToList however, that's where I'm getting stuck.
Currently the code looks lke this (roughly, I've taken some of the search criteria out to make it easier to display)
var Widgets = from b in _caspEntities.Widgets.Include("WidgetRegionLogs")
.Include("WidgetStatusLogs").Include("WidgetVoltageTests")
select b;
IEnumerable<Widget> results = Widgets.ToList();
if (comboBoxRegion.SelectedValue.ToString() != "0")
{
results = from b in results
where b.CurrentRegionLog != null && b.CurrentRegionLog.RegionId == int.Parse(comboBoxRegion.SelectedValue.ToString())
select b;
}
if (comboBoxStatus.SelectedValue != null)
{
results = from b in results
where b.CurrentStatusLog != null && b.CurrentStatusLog.StatusId == comboBoxStatus.SelectedValue.ToString()
select b;
}
if (txtCode.Text.Trim().Length > 0)
{
results = from b in results
where b.CodeNumber == txtCode.Text.Trim()
select b;
}
dataGridViewWidget.DataSource = results.ToList();
I can write the SQL easily enough, essentially the model is simple, I have a Widget it has a RegionLog and a StatusLog, both of which store a history. The current region and status are retrieved from this by grouping by WidgetID and selecting the most recent Date Updated (and then going off to Region and Status tables to get the actual value).
So, I need to translate this into LINQ, but to be honest I don't have a clue but am ken and willing to learn. In my head, I think I need to add some better where clauses, and then do the Widget.toList after I have applied the where clauses. I'm struggling with the CurrentRegionLog and CurrentStatusLog concepts as they are not populated until I run the IEnumerable.
If anyone can give some pointers, I'd be grateful,
Thanks
Edit - Added
public BatteryRegionLog CurrentRegionLog
{
get { return _currentRegionLog; }
}
private BatteryRegionLog _currentRegionLog
{
get
{
if (this.BatteryRegionLogs.Count > 0)
{
BatteryRegionLog log = this.BatteryRegionLogs.OrderByDescending(item => item.LastModifiedDate).First();
return log;
}
else
{
return null;
}
}
}
You can compose the query like this:
if (comboBoxRegion.SelectedValue.ToString() != "0")
{
var id = int.Parse(comboBoxRegion.SelectedValue.ToString()
Widgets = from b in Widgets
let currentRegionLog =
b.BatteryRegionLogs
.OrderByDescending(item => item.LastModifiedDate)
.FirstOrDefault()
where currentRegionLog.RegionId == id)
select b;
}
... // Same for the other criteria.
dataGridViewWidget.DataSource = Widgets.ToList();
The whole query is not executed before you do ToList(). As everything is translated to SQL you don't need the null check b.CurrentRegionLog != null. SQL will evaluate b.CurrentRegionLog.RegionId == id just fine when there is no CurrentRegionLog.
Edit
Since CurrentRegionLog is a calculated property of your Widget class it cannot be translated into SQL. I made an effort to incorporate the code of calculated property into the query in a way that only the basic navigation property is used, so EF can translate it to SQL again.
try remove this line:
IEnumerable<Widget> results = Widgets.ToList();
and just use the Widgets variable you get in at the top
The .ToList() goes to the database and materialiaze all the data into entities.
if you don't call the .ToList() the query is still "open" for a where clause

How to use the return value of a function in a linq where clause?

I'm trying to create a general search query against an EF entity type (person). In general, the search takes a string, splits it by commas, and then searches for people whose various attributes contain all of the key words.
I have a function called getProperties(Person p) that takes an entity (overridden by entity type), and returns a string of the various relevant properties joined together with a delimiter... such as:
John~Doe~Team A~Full Time
If the user searches for "Team A, Full" person corresponding to the above flattened entity should be returned... however, if the enter "John, Smith" it shouldn't.
I think the following looks right, but it just doesn't work...
public IEnumerable<Person> SearchPeople(string searchString)
{
if (searchString == null || string.IsNullOrEmpty(searchString.Trim()))
return base._objectSet.ToList();
string[] SearchWords = searchString.Split(',').Select(s => s.Trim()).ToArray();
return (from person
in base._objectSet
let t = (getProperties(person))
where SearchWords.All(word => t.Contains(word))
select person).ToList();
}
and the getProperties function is:
public static string getProperties(Person p)
{
string[] values = { p.Surname, p.GivenName, p.Team, p.Status };
return values.Aggregate((x, y) => String.IsNullOrEmpty(y) ? x : string.Concat(x, "~", y));
}
Does anyone see where I'm going wrong?
Edit
No exceptions are raised, but when I step through the code, when I get to the linq, it steps into the dispose method of the unitofwork that is hosting the query. Very odd.
If I change it so that it searches against a hard-coded string, it works as expected:
var test = (from person
in base._objectSet
where SearchWords.All(word => "John~Doe~Team A~Full Time".Contains(word))
select person).ToList();
well, it works in that it matches the queries I expect it to, but as it's static, it returns every person record (pretty much like having where(true) =P)
Edit the Second
Even odder is that if I store the results into a var, then return the var with a breakpoint on the return, execution never hits the breakpoint... this linq is like a black hole... I can step into it, but it never returns me back to my SearchPeople method... it just disposes the context and forgets about it.
Edit the Third
If I don't call ToString() right away and look at the linq expression in debugger, it says "Linq to Entities does not recognize the method getProperties(Person)" Looks like it was silently choking on my method... any way to use my method without linq choking on it?
You are returning a List? try the return type to be a List or change the .ToList() to be AsEnumerable()
public List<Person> SearchPeople(string searchString)
{
if (searchString == null || string.IsNullOrEmpty(searchString.Trim()))
return base._objectSet.ToList();
string[] SearchWords = searchString.Split(',').Select(s => s.Trim()).ToArray();
return (from person
in base._objectSet
let t = (getProperties(person))
where SearchWords.All(word => t.Contains(word))
select person).ToList();
}
Well, after finding out that linq 2 entites doesn't like methods (as is doesn't know how to translate it to sql), i rewrote my linq is a very tedious but functioning manner:
var people = from p
in base._objectSet
where SearchWords.All(p.GivenName.Contains(word) || p.Surname.Contains(word) || p.(???).Contains(word) || etc.)
select p;
Annoying, but there you go.

Why does my Linq Where clause produce more results instead of less?

I just had the weirdest debug experience in a very long time. It's a bit embarassing to admit, but it lead me to be believe that my Linq query produces MORE results when adding an additional Where clause.
I know it's not possible, so I've refactored my offending function plus the unit test belonging to it into this:
[Test]
public void LoadUserBySearchString()
{
//Setup
var AllUsers = new List<User>
{
new User
{
FirstName = "Luke",
LastName = "Skywalker",
Email = "luke#jedinet.org"
},
new User
{
FirstName = "Leia",
LastName = "Skywalker",
Email = "faeryprincess#winxmail.com"
}
};
//Execution
List<User> SearchResults = LoadUserBySearchString("princess", AllUsers.AsQueryable());
List<User> SearchResults2 = LoadUserBySearchString("princess Skywalker", AllUsers.AsQueryable());
//Assertion
Assert.AreEqual(1, SearchResults.Count); //test passed!
Assert.AreEqual(1, SearchResults2.Count); //test failed! got 2 instead of 1 User???
}
//search CustID, fname, lname, email for substring(s)
public List<User> LoadUserBySearchString(string SearchString, IQueryable<User> AllUsers)
{
IQueryable<User> Result = AllUsers;
//split into substrings and apply each substring as additional search criterium
foreach (string SubString in Regex.Split(SearchString, " "))
{
int SubStringAsInteger = -1;
if (SubString.IsInteger())
{
SubStringAsInteger = Convert.ToInt32(SubString);
}
if (SubString != null && SubString.Length > 0)
{
Result = Result.Where(c => (c.FirstName.Contains(SubString)
|| c.LastName.Contains(SubString)
|| c.Email.Contains(SubString)
|| (c.ID == SubStringAsInteger)
));
}
}
return Result.ToList();
}
I have debugged the LoadUserBySearchString function and asserted that the second call to the function actually produces a linq query with two where clauses instead of one. So it seems that the additional where clause is increasing the amount of results.
What's even more weird, the LoadUserBySearchString function works great when I test it by hand (with real users from the database). It only shows this weird behavior when running the unit test.
I guess I just need some sleep (or even an extended vacation). If anyone could please help me shed some light on this, I could go stop questioning my sanity and go back to work.
Thanks,
Adrian
Edit (to clarify on several responses I go so far): I know it looks like it is the or clause, but unfortuantely it is not that simple. LoadUserBySearchString splits the search string into several strings and attaches a Where clause for each of them. "Skywalker" matches both luke and Leia, but "princess" only matches Leia.
This is the Linq query for the search string "princess":
+ Result {System.Collections.Generic.List`1[TestProject.Models.User].Where(c => (((c.FirstName.Contains(value(TestProject.Controllers.SearchController+<>c__DisplayClass1).SubString) || c.LastName.Contains(value(TestProject.Controllers.SearchController+<>c__DisplayClass1).SubString)) || c.Email.Contains(value(TestProject.Controllers.SearchController+<>c__DisplayClass1).SubString)) || (c.ID = value(TestProject.Controllers.SearchController+<>c__DisplayClass3).SubStringAsInteger)))} System.Linq.IQueryable<TestProject.Models.User> {System.Linq.EnumerableQuery<TestProject.Models.User>}
And this is the Linq clause for the search string "princess Skywalker"
+ Result {System.Collections.Generic.List`1[TestProject.Models.User].Where(c => (((c.FirstName.Contains(value(TestProject.Controllers.SearchController+<>c__DisplayClass1).SubString) || c.LastName.Contains(value(TestProject.Controllers.SearchController+<>c__DisplayClass1).SubString)) || c.Email.Contains(value(TestProject.Controllers.SearchController+<>c__DisplayClass1).SubString)) || (c.ID = value(TestProject.Controllers.SearchController+<>c__DisplayClass3).SubStringAsInteger))).Where(c => (((c.FirstName.Contains(value(TestProject.Controllers.SearchController+<>c__DisplayClass1).SubString) || c.LastName.Contains(value(TestProject.Controllers.SearchController+<>c__DisplayClass1).SubString)) || c.Email.Contains(value(TestProject.Controllers.SearchController+<>c__DisplayClass1).SubString)) || (c.ID = value(TestProject.Controllers.SearchController+<>c__DisplayClass3).SubStringAsInteger)))} System.Linq.IQueryable<TestProject.Models.User> {System.Linq.EnumerableQuery<TestProject.Models.User>}
Same as above, just with one additional where clause.
This is a nice little gotcha.
What is happening is that, because of anonymous methods, and deferred execution, you're actually not filtering on "princess". Instead, you're building a filter that will filter on the contents of the subString variable.
But, you then change this variable, and build another filter, which again uses the same variable.
Basically, this is what you will execute, in short form:
Where(...contains(SubString)).Where(...contains(SubString))
so, you're actually only filtering on the last word, which exists in both, simply because by the time these filters are actually applied, there is only one SubString value left, the last one.
If you change the code so that you capture the SubString variables inside the scope of the loop, it'll work:
if (SubString != null && SubString.Length > 0)
{
String captured = SubString;
Int32 capturedId = SubStringAsInteger;
Result = Result.Where(c => (c.FirstName.Contains(captured)
|| c.LastName.Contains(captured)
|| c.Email.Contains(captured)
|| (c.ID == capturedId)
));
}
Your algorithm amounts to "select records which match any of the words in the search string".
This is because of deferred execution. The query is not actually performed until you call the .ToList(). If you move the .ToList() inside the loop, you'll get the behaviour you want.

Categories

Resources