LINQ search filter logic - c#

I have apartment entity and I want to get apartments based on my filters.
this is my search entity
public class Search
{
public bool isStudio {get;set;}
public bool isNoPlanning {get;set;}
public bool isMultiRoom {get;set;}
public int[] NumberOfRooms {get;set;}
}
this is my current search logic.
var apartments = buildRepost.Get(buildId).Where(condStates =>
(searchModel.NumberOfRooms != null
&& searchModel.NumberOfRooms.Contains(condStates.RoomsCount.ToString())
|| ((searchModel.IsStudio && condStates.IsStudio))
|| ((searchModel.IsNoPlanning && condStates.IsFreePlaning))
|| ((searchModel.IsMultiRoom && condStates.RoomsCount >= 4)));
The problem with this logic is that I got the wrong result when all fields are false and null. For example when isStudio, IsNoplaning and isMultiRoom are false and numberofRooms is null I should have got all apartments but instead, I got an empty array. Any help?

the searchModel.NumberOfRooms != null checker in the where clause causes the problem, you are not mathcing it with any of the condStates properties and the searchModel.Is..... properties
Make your searchModel as a checker in an if statement then build the query from the if searchModel conditions.
var query = buildRepost.Get(buildId).AsQueryable();
if (searchModel.NumberOfRooms != null)
{
query = query.Where(condStates => searchModel.NumberOfRooms.Contains(condStates.RoomsCount.ToString());
}
if (searchModel.IsStudio)
{
query = query.Where(condStates => condStates.IsStudio);
}
if (searchModel.IsNoPlaning)
{
query = query.Where(condStates => condStates.IsFreePlaning)
}
if (searchModel.IsMultiRoom)
{
query = query.Where(condStates => condStates.RoomsCount >= 4)
}
var results = query.ToList()

(searchModel.NumberOfRooms != null
&& searchModel.NumberOfRooms.Contains(condStates.RoomsCount.ToString())
This is always going to be false when the NumberOfRooms is null (due to the null check), and given that the other bool values are all false, you will get no results.
Instead if you change to:
(searchModel.NumberOfRooms == null
|| searchModel.NumberOfRooms.Contains(condStates.RoomsCount.ToString())
You will either get everything (when the NumberOfRooms is null, or just the records that match the RoomsCount (when the NumberOfRooms is not null).
Note that in this case, if NumberOfRooms is null you will still return everything regardless of your bool filters. Which seems to be what your code requires, but I'm not sure is what you actually require, so you might want to check that.

Related

LINQ .Where .Max and Writing to File

I have a program which writes its output to log files to folders in the following format:
Car7 Lap1 00-00-21-5290000
Everything is working fine, but I want to append a # to the filename of the fastest lap time for each car. In order to do so, I placed the following LINQ query inside my car object to act on the List property of Car:
public List<Lap> Laps { get; set; } = new List<Lap>();
public TimeSpan FastestLap => Laps.Where(lap => lap.Number is not null and not 0).Min(lap => lap.LapTime);
I'm using the .Where clause as laps can sometimes be null or 0, and when I write to disk things initially appear to be working:
Car8 Lap39 00-01-07-8900000#
However, only 8 cars get written to disk as opposed to the full field of 20 cars. If I remove only the .Where part of my property above, all the cars and car folders write properly, except the Lap 0 files are typically marked as the fastest lap (which makes sense since they contain incomplete times).
My writing to disk method looks like this:
foreach (var car in carList)
{
Directory.CreateDirectory(#$"{outputPath}\{logFileName}\Car{car.Number}");
foreach (var lap in car.Laps)
{
using TextWriter tw = new StreamWriter(#$"{outputPath}\{logFileName}\Car{car.Number}\Car{car.Number} Lap{lap.Number} {lap.LapTime.ToString().Replace(":", "-").Replace(".", "-")}{(lap.LapTime == car.FastestLap ? "#" : "")}.csv");
WriteCsvHeader(tw);
foreach (var telemetryRecord in lap.UniqueTelemetryRecords)
{
WriteCsvLine(tw, lap, telemetryRecord);
}
}
}
Is there a way to prevent this from happening so that all cars and car folders get written to disk?
In this case, use a getter to perform a bit more advanced logic when retrieving the value.
Replace this line:
public TimeSpan FastestLap => Laps.Where(lap => lap.Number is not null and not 0).Min(lap => lap.LapTime);
With this:
public TimeSpan FastestLap
{
get
{
if(Laps.Where(lap => lap.Number is not null and not 0).Count() > 0)
return Laps.Where(lap => lap.Number is not null and not 0).Min(lap => lap.LapTime);
else
return Laps.Min(lap => lap.LapTime);
}
}
This is essentially saying that if any non zero laps exist, get the lowest value. Otherwise, it'll just return the zero value.
One option would be to implement IComparable<T> on your class:
public class Lap : IComparable<Lap>
{
public int CompareTo(Lap other)
{
//if both have nulls or zero they are equal
if((this.Number == null || this.Number == 0) && (other == null || other.Number == null || other.Number == 0) return 0;
// If other is null or the other.Number is null or zero this instance is first in sort order
if (other == null || other.Number == null || other.Number == 0) return -1;
//if this instance has null or zero for the number and other doesn't this instance comes after other
if((this.Number == null || this.Number == 0) && other != null && other.Number != null && other.Number != 0) return 1;
//Comparison depends on this.LapTime to other.LapTime
return LapTime.CompareTo(other.LapTime);
}
......
}
Then you could just call .Sort() on the List<Lap> and your first item in the List will be your fastest lap that is not null or zero. Append the # to the first item in the list and you don't need an extra method in your class.

Where function on list with multiple conditions returns errors

I'm trying to filter a list based on a few criteria and the .Where() function gives me an error in 2 parts of the same method.
if (string.IsNullOrWhiteSpace(champs))
{
data = dal.GetVueTache().Where(t =>
t.ProjetDescription64.ToLower().Contains(filtre.ToLower())
// *This Line || t.ProjetDescription256.ToLower().Contains(filtre.ToLower())
|| t.Description256.ToLower().Contains(filtre.ToLower())
||t.ResponsableNomCourt.ToLower().Contains(filtre.ToLower())
|| t.PrioriteDesc.ToLower().Contains(filtre.ToLower())
).ToList();
}
If I use any of the previous conditions except the one on the nullable field alone I get a perfectly filtered list on that criteria, if I add an OR "||" then I get a System.NullReferenceException on the first criteria.
I also have a similar issue in another part of the same method
else
{
data = dal.GetVueTache().Where(t =>
t.GetType().GetProperty(champs).GetValue(t).ToString().ToLower().Contains(filtre.ToLower())
).ToList();
}
This one filters my list based on the criteria "filtre" on the property "champs". It works on every property but the second one, which is a nullable one. I understand that this is where the issue comes from, but I can't find a way to test if the property is null before evaluating it and work around this from inside the .Where() Method.
Any advice will be greatly appreciated!!
Edit :
Thanks to Ivan Stoev for his solution!
The correct syntax for testing the null value in the first case is:
|| (t.ProjetDescription256 != null && t.ProjetDescription256.ToLower().Contains(filtre.ToLower()))
In the second case:
(t.GetType().GetProperty(champs).GetValue(t) != null && t.GetType().GetProperty(champs).GetValue(t).ToString().ToLower().Contains(filtre.ToLower()))
Just do a null check either the old way:
|| (t.ProjetDescription256 != null && t.ProjetDescription256.ToLower().Contains(filtre.ToLower()))
or the C# 6 way (utilizing the null conditional operator):
|| t.ProjetDescription256?.ToLower().Contains(filtre.ToLower()) == true
Btw, you can greatly simplify similar checks and avoid such errors by writing a simple custom extension methods like this:
public static class StringExtensions
{
public static bool ContainsIgnoreCase(this string source, string target)
{
return source == null ? target == null : target != null && source.IndexOf(target, StringComparison.CurrentCultureIgnoreCase) >= 0;
}
}
so your snippet becomes simply:
data = dal.GetVueTache().Where(
t => t.ProjetDescription64.ContainsIgnoreCase(filtre)
|| t.ProjetDescription256.ContainsIgnoreCase(filtre)
|| t.Description256.ContainsIgnoreCase(filtre)
|| t.ResponsableNomCourt.ContainsIgnoreCase(filtre)
|| t.PrioriteDesc.ContainsIgnoreCase(filtre)
).ToList();

Sequence contains no matching element Error using Boolean

bool Postkey =
statement
.ThreadPostlist
.First(x => x.ThreadKey == ThreadKey && x.ClassKey == classKey)
.PostKey;
This Ling query is giving me "Sequence contains no matching element" but I know I can use .FirstorDefault(). When I use .FirstorDefault() it will return me false, the default value for bool, when there are no matching records.
But I get a "Object not set to an instance of an object" error. I need to check the bool for null with .HasValue and .Value. I don't know how to do it.
Here is how you can use a nullable bool to solve this:
bool? postKey = null;
// This can be null
var post = statement.ThreadPostlist.FirstOrDefault(x=>x.ThreadKey == ThreadKey && x.ClassKey == classKey);
if (post != null) {
postKey = post.PostKey;
}
// Now that you have your nullable postKey, here is how to use it:
if (postKey.hasValue) {
// Here is the regular bool, not a nullable one
bool postKeyVal = postKey.Value;
}
You could do:-
bool? postkey = threadPostList
.Where(x=>x.ThreadKey == threadKey && x.ClassKey == classKey)
.Select(x => (bool?)x.PostKey)
.DefaultIfEmpty()
.First();
I think that better captures the intent of what you are trying to accomplish.
If you want to treat a null value as false (and don't want to use a nullable bool), you could just check if the resulting post is null before referencing the .PostKey property, like this:
var threadPost = statement.ThreadPostlist.FirstOrDefault(x =>
x.ThreadKey == ThreadKey && x.ClassKey == classKey);
bool PostKey = threadPost != null && threadPost.PostKey;
Or, a longer form would be:
bool PostKey;
if (threadPost != null)
{
PostKey = threadPost.PostKey;
{
else
{
PostKey = false;
}

Matching on search attributes selected by customer on front end

I have a method in a class that allows me to return results based on a certain set of Customer specified criteria. The method matches what the Customer specifies on the front end with each item in a collection that comes from the database. In cases where the customer does not specify any of the attributes, the ID of the attibute is passed into the method being equal to 0 (The database has an identity on all tables that is seeded at 1 and is incremental). In this case that attribute should be ignored, for example if the Customer does not specify the Location then customerSearchCriteria.LocationID = 0 coming into the method. The matching would then match on the other attributes and return all Locations matching the other attibutes, example below:
public IEnumerable<Pet> FindPetsMatchingCustomerCriteria(CustomerPetSearchCriteria customerSearchCriteria)
{
if(customerSearchCriteria.LocationID == 0)
{
return repository.GetAllPetsLinkedCriteria()
.Where(x => x.TypeID == customerSearchCriteria.TypeID &&
x.FeedingMethodID == customerSearchCriteria.FeedingMethodID &&
x.FlyAblityID == customerSearchCriteria.FlyAblityID )
.Select(y => y.Pet);
}
}
The code for when all criteria is specified is shown below:
private PetsRepository repository = new PetsRepository();
public IEnumerable<Pet> FindPetsMatchingCustomerCriteria(CustomerPetSearchCriteria customerSearchCriteria)
{
return repository.GetAllPetsLinkedCriteria()
.Where(x => x.TypeID == customerSearchCriteria.TypeID &&
x.FeedingMethodID == customerSearchCriteria.FeedingMethodID &&
x.FlyAblityID == customerSearchCriteria.FlyAblityID &&
x.LocationID == customerSearchCriteria.LocationID )
.Select(y => y.Pet);
}
I want to avoid having a whole set of if and else statements to cater for each time the Customer does not explicitly select an attribute of the results they are looking for. What is the most succint and efficient way in which I could achieve this?
Criteria that are not selected are always zero, right? So how about taking rows where the field equals the criteria OR the criteria equals zero.
This should work
private PetsRepository repository = new PetsRepository();
public IEnumerable<Pet> FindPetsMatchingCustomerCriteria(CustomerPetSearchCriteria customerSearchCriteria)
{
return repository.GetAllPetsLinkedCriteria()
.Where(x => (customerSearchCriteria.TypeID == 0 || x.TypeID == customerSearchCriteria.TypeID)&&
(customerSearchCriteria.FeedingMethodID == 0 || x.FeedingMethodID == customerSearchCriteria.FeedingMethodID) &&
(customerSearchCriteria.FlyAblityID == 0 || x.FlyAblityID == customerSearchCriteria.FlyAblityID) &&
(customerSearchCriteria.LocationID == 0 || x.LocationID == customerSearchCriteria.LocationID))
.Select(y => y.Pet);
}
Alternatively, if this is something you find yourself doing alot of, you could write an alternate Where extension method that either applies the criteria or passes through if zero, and chain the calls instead of having one condition with the criteria anded. Then you'd do the comparision for the criteria == 0 just once per query, not for every unmatched row. I'm not sure that it's worth the - possible - marginal performance increase, you'd be better off applying the filters in the database if you want a performance gain.
Here it is anyway, for the purposes of edification . . .
static class Extns
{
public static IEnumerable<T> WhereZeroOr<T>(this IEnumerable<T> items, Func<T, int> idAccessor, int id)
{
if (id == 0)
return items;
else
return items.Where(x => idAccessor(x) == id);
}
}
private PetsRepository repository = new PetsRepository();
public IEnumerable<Pet> FindPetsMatchingCustomerCriteria(CustomerPetSearchCriteria customerSearchCriteria)
{
return repository.GetAllPetsLinkedCriteria()
.WhereZeroOr(x => x.TypeID, customerSearchCriteria.TypeID)
.WhereZeroOr(x => x.FeedingMethodID, customerSearchCriteria.FeedingMethodID)
.WhereZeroOr(x => x.FlyAblityID, customerSearchCriteria.FlyAblityID)
.WhereZeroOr(x => x.LocationID, customerSearchCriteria.LocationID);
}
Looks like you're using a stored procedure and you're getting all records first and then doing your filtration. I suggest you filter at the stored procedure level, letting the database do the heavy lifting and any micro filtration that you need to do afterwords will be easier. In your sproc, have your params default to NULL and make your properties nullable for the criteria object so you can just pass in values and the sproc will(should) be corrected to work with these null values, i.e.
private PetsRepository repository = new PetsRepository();
public IEnumerable<Pet> FindPetsMatchingCustomerCriteria(CustomerPetSearchCriteria customerSearchCriteria)
{
return repository.GetAllPetsLinkedCriteria(customerSearchCriteria.TypeID,customerSearchCriteria.FeedingMethodID,customerSearchCriteria.FlyAblityID,customerSearchCriteria.LocationID).ToList();
}
I'm not seeing an elegant solution. May be this:
IEnumerable<Pet> FindPetsMatchingCustomerCriteria(CustomerPetSearchCriteria customerSearchCriteria)
{
return repository.GetAllPetsLinkedCriteria()
.Where(x =>
Check(x.TypeID, customerSearchCriteria.TypeID) &&
Check(x.FeedingMethodID, customerSearchCriteria.FeedingMethodID) &&
Check(x.FlyAblityID, customerSearchCriteria.FlyAblityID) &&
Check(x.LocationID, customerSearchCriteria.LocationID))
.Select(x => x.Pet);
}
static bool Check(int petProperty, int searchCriteriaProperty)
{
return searchCriteriaProperty == 0 || petProperty == searchCriteriaProperty;
}

FindAll in a c# List, but varying search terms

List<DTOeduevent> newList = new List<DTOeduevent>();
foreach (DTOeduevent e in eduList.FindAll(s =>
s.EventClassID.Equals(cla)
&& s.LocationID.Equals(loc)
&& s.EducatorID.Equals(edu)))
newList.Add(e);
cla, loc, edu can be (null or empty) or supplied with values--
basically how can I simply return the original list (eduList) if cla, loc, edu are all null
or search by loc, search by loc, edu, search by edu, cla -- etc........
my sample code only makes a new list if all 3 vars have values--
is there an elegant way to do this, without brute force if statements?
List<DTOeduevent> newList = eduList.FindAll(s =>
(cla == null || s.EventClassID.Equals(cla))
&& (loc == null || s.LocationID.Equals(loc))
&& (edu == null || s.EducatorID.Equals(edu)));
Assuming the values are Nullable value types or classes. If they're strings, you could replace cla == null with String.IsNullOrEmpty(cla).
IEnumerable<DTOeduevent> newList = eduList;
if (cla != null)
{
newList = newList.Where(s => s.EventClassID == cla);
}
if (loc != null)
{
newList = newList.Where(s => s.LocationID == loc);
}
if (edu != null)
{
newList = newList.Where(s => s.EducatorID == edu);
}
newList = newList.ToList();
Due to deferred execution, the Where statements should all execute at once, when you call ToList; it will only do one loop through the original list.
I would personally lean towards something that encapsulated the logic of what you seem to be doing here: checking that a found id is equal to some search id. The only wrinkle is how to get that check for null or empty in there first.
One way to do that is by using a static extension method:
public static class DtoFilterExtensions
{
public static bool IsIdEqual(this string searchId, string foundId) {
Debug.Assert(!string.IsNullOrEmpty(foundId));
return !string.IsNullOrEmpty(searchId) && foundId.Equals(searchId);
}
}
I would also lean towards using LINQ and IEnumerable<> as Domenic does, even though you could make it work with List.FindAll just as easily. Here would be a sample usage:
public void Filter(string cla, string loc, string edu) {
var startList = new List<DTOeduevent>();
var filteredList = startList
.Where(x => x.classId.IsIdEqual(cla) && x.locationId.IsIdEqual(loc) && x.educatorId.IsIdEqual(edu));
Show(filteredList.ToList());
}
In your own code of course you have got that start list either in a member variable or a parameter, and this assumes you have got some method like Show() where you want to do something with the filtered results. You trigger the deferred execution then, as Domenic explained with the ToList call (which is of course another extension method provided as part of LINQ).
HTH,
Berryl

Categories

Resources