How to ignore null properties in the search criteria - c#

I have got a bad requirement to do; anyway I have to implement that in my application.
I have a Track class
public class Track
{
public string Name { get; set; }
public string City { get; set; }
public string Country { get; set; }
}
and i have some test data
List<Track> Records = new List<Track>
{
new Track { City = "a", Name = "a", Country = "i" }, // Track 1
new Track { City = "b", Name = "b", Country = "i" }, // Track 2
new Track { City = "a", Name = null, Country = "J" }, // Track 3
new Track { City = "c", Name = "a", Country = "J" }, // Track 4
new Track { City = "b", Name = "a", Country = null}, // Track 5
};
Requirement is i should query the data from Records based on values passed. If any of the property is null then search criteria should ignore that property. It should query based on NonNull properties.
Example:
If i query for City = a, Name = "b", Country = "i" then Result is: Track 1 & Track 3
If i query for City = c, Name = "p", Country = "w" then Result is: Track 4
Name & Country have null values in Track 3 & Track 5.So it will ignore in search. Hope it is clear
I finally land up with below logic
var filterRecords = new List<Track>();
if (!Records.Any(t => string.IsNullOrWhiteSpace(t.City)))
{
filterRecords = Records.Where(c => c.City == _city).ToList(); // Here _city is the method parameter.. assume "a"
}
if (!Records.Any(t => string.IsNullOrWhiteSpace(t.Country)))
{
filterRecords = filterRecords.Where(c => c.City == _country).ToList(); // Here _country is the method parameter.. assume "c"
}
Track class has 12 properties. Checking for 12 times like above is not good sign.
I would like to achieve this by using LINQ or any other which is simple.
Any suggestions please?.

Best solution came to my mind is to build aggregate filter (You can use your Track object for that, because it already has all possible properties for filtering collection, and they are nullable):
Track filter = records.Aggregate(
new Track() { City = _city, Country = _country, Name = _name },
(f, t) => new Track()
{
City = String.IsNullOrEmpty(t.City) ? null : f.City,
Country = String.IsNullOrEmpty(t.Country) ? null : f.Country,
Name = String.IsNullOrEmpty(t.Name) ? null : f.Name
},
f => f);
This will require only one iteration over collection to define which fields has null. Then simply apply filter to your records:
var query = from t in Records
where (filter.City == null || t.City == filter.City) &&
(filter.Country == null || t.Country == filter.Country) &&
(filter.Name == null || t.Name == filter.Name)
select t;

You can easily chain these expressions together to create something like the following. Enumerating the records X times is going to slow down your code. Example:
var actualResult = Records
.Where(x => x.City == _city || string.IsNullOrEmpty(x.City))
.Where(x => x.Country == _country || string.IsNullOrEmpty(x.Country))
/// .... and so on
.ToList()
Now, if you're looking to not just write 12 lines of code, there are more complicated solutions that involve Reflection and mapping, but that's not really needed in this situation. If your property count is huge, then maybe it might be worth it. But 12 properties is small enough that there isn't much code smell here to me.

If I'm understanding, it should be this simple:
var results = Records.Select(p => p.City != null &&
p.Country != null &&
p.Name != null).ToList();

do the quality and quantity of results returned matter to you?
I assume you have mechanisms to handle the quantity.
You can validate your search keywords before throwing queries. Do you only care about nulls? How about redundant keywords?
I would consider:
1. validate keywords - 12 can be looped.
2. Build the search keyword strong
3. Shoot a query
Seems pretty straightforward, unless there's more to it than what you described here.
Please recorrect me, if my understanding of your question is not in the expected direction.

something like this
var listRecords = from track in Records
where (track.city == _city && !string.IsEmpyOrNull(track.city)) &&
(track.Name== _name && !string.IsEmpyOrNull(track.Name))
select track;
and you can add the rest of the conditions

Related

Having poor performance when getting data from cosmos db by code

We have aroung 140K documents in a collection, and a typical search returns around 6 MB of data.
This process is taking around 30 to 40 seconds, and I would like to know why, and how can I improve it?
Sample data to see how the structure looks like: https://pastebin.com/0eJJfVKB
_id is the primarmy key, and there's a compound index on HotelID, GiataCityID, and CountryCode.
This is how I'm getting my data from cosmos by code:
The first part is to only get these data , as I figured out why return everything, just get what I need, that should speed things up.
Would appreciate any input, and ready to provide more details.
var hoteldetails = new List<HotelDetails>();
var projection = Builders<HotelDetails>.Projection.Expression(
t => new HotelDetails
{
Id = t.Id,
PropertyName = t.PropertyName,
GiataCityId = t.GiataCityId,
City = t.City,
CityId = t.CityId,
SupplierList = t.SupplierList,
CountryCode = t.CountryCode,
CountryName = t.CountryName,
longitude = t.longitude,
latitude = t.latitude,
Images = t.Images,
HotelId = t.HotelId,
SupplierRoomList = t.SupplierRoomList
});
this.GetDBClient();
var database = this.client.GetDatabase(this.settings.DataBase);
var collection = database.GetCollection<HotelDetails> (MongoCollections.HotelDetails.ToString());
if (HotelIDs != null && HotelIDs.Count > 0)
{
hoteldetails = await collection
.Aggregate()
.Match(x => x.CountryCode == CountryCode && (x.CityId == CityId || x.GiataCityId ==CityId.ToString())
&& x.SupplierList.Any(y => HotelIDs.Contains(y.HotelId) && y.SupplierName == SupplierNameType))
.Project(projection)
.Limit(Limit)
.ToListAsync();
}
The RU data page
RUs
I tried to use projecting to only get the properties i need, that sped things up a bit but not by a whole lot.
I tried to play with the filter, but really to no effect.

How do I select rows from database elegantly with multiple conditions (which could be null)?

I would like to select data from database according to what the user has entered. However, if that condition is not provided (null), it will ignore that condition and only take the provided conditions to filter out the relevant rows.
I find my solution (pseudocode) to be very inefficient and ugly and I hope someone can share his/her knowledge with me.
This is what I have tried:
'''
//selectConditions - input by user
var dataList = from data in entities.StudentsData
orderby data.Id
select data;
if (selectCondition.Age != null)
{
dataList = filter(dataList, selectCondition.Age);
}
if (selectCondition.Gender != null)
{
dataList = filter(dataList, selectCondition.Gender);
}
//may contain more conditions
//dataList now contains all rows with conditions specified by user
'''
So If I have a table
Name Age Gender
Tom 12 Male
Mary 13 Female
May 15 Female
Jack 14 Male
Case 1: Conditions are
Gender: Male
Age: null
I should get
Tom 12 Male
Jack 14 Male
Case 2: Conditions are
Gender: Female
Age: 15
I should get
May 15 Female
You can use the power of ||
entitieslist.Where(stud =>
(!selectCondition.Age.HasValue || selectCondition.Age == stud.Age)
&& (!selectCondition.Gender.HasValue || selectCondition.Gender== stud.Gender)
.ToList()
I don't think you can apply all filters in a single query. You can check for values that are not null and add appropriate where clauses.
A sample given below:
var datalist = entities.StudentsData.OrderBy(stud => stud.id);
//filterCondition contains the filter values
if(filterCondition.Age.HasValue)
{
datalist = datalist.Where(stud => stud.Age == filterCondition.Age);
}
if(filterCondition.Gender.HasValue)
{
datalist = datalist.Where(stud =>
stud.Gender.Equals(filterCondition.Gender))
}
//More filters can be added as per your requirement.
datalist.ToList();
I hope this will help you.
You can return true if the condition was null
var dataList = from data in entities.StudentsData
where
(selectCondition.Age == null ? true :data.Age ==selectCondition.Age )&&
(selectCondition.Gender == null ? true :data.Gender ==selectCondition.Gender )
//...(check if is null ? return true: your conidtion)
orderby data.Id
select data;
I have done this several ways
One similar, with an IQueryable List, like this
public List<Product1> GetProducts(long? cId, string productCode)
{
var age = false; //Added in a hurry
var gender = "M"; //Added in a hurry
var products =
_unitOfWork.StagingProductRepository.GetMany(
x => x.CID == cId && x.ProductCode == productCode );
if(age)
{
products = products.Where(x => x.ProductAge == age);
}
if (gender != string.Empty)
{
products = products.Where(x => x.Gender.Trim() == gender.Trim());
}
return products.ToList();
}
Also done the same idea but with Raw sql, appending the And clauses
Lastly, Q for you.
Is your filter method enumerating the data?

How do I use a variable with my WHERE clause in my Entity Framework query

I'm running a query in my project with multiple joins. I want to provide the WHERE clause with a variable instead of hard coded as it is now but cannot seem to get the correct syntax.
var data = (from a in db.StudentData
join b in db.Contacts on a.SID equals b.SID
join c in db.Addresses on a.SID equals c.SID
join d in db.EntryQuals.DefaultIfEmpty() on a.SID equals d.SID
where a.SID == searchTxt
select new
{
ID = a.SID,
Birthdate = a.BIRTHDTE,
FirstName = a.FNAMES,
PreviousName = a.PREVSURNAME,
EntryQualAwardID = d.ENTRYQUALAWARDID,
AwardDate = d.AWARDDATE
}).ToList();
How can I get my WHERE clause to work with a variable (ie: a.[ fieldVar ] ) where fieldVar could be "SID" as it is in the code currently.
When dealing with user select-able search criteria you will need to code for the possible selections. When dealing with building searches I recommend using the Fluent syntax over the Linq QL syntax as it builds an expression that is easy to conditionally modify as you go. From there you can use a Predicate & PredicateBuilder to dynamically compose your WHERE condition.
Jacques solution will work, but the downside of this approach is that you are building a rather large & complex SQL statement which conditionally applies criteria. My preference is to conditionally add the WHERE clauses in the code to ensure the SQL is only as complex as it needs to be.
If you want to do something like a smart search (think Google with one text entry to search across several possible fields)
var whereClause = PredicateBuilder.False<StudentData>();
int id;
DateTime date;
if(int.TryParse(searchTxt, out id))
whereClause = whereClause.Or(x => x.SID == id);
else if(DateTime.TryParse(searchTxt, out date))
whereClause = whereClause.Or(x => x.BirthDate == date);
else
whereClause = whereClause.Or(x => x.FirstName.Contains(searchTxt));
var data = db.StudentData
.Where(whereClause)
.Select(a => new
{
ID = a.SID,
Birthdate = a.BIRTHDTE,
FirstName = a.FNAMES,
PreviousName = a.PREVSURNAME,
EntryQualAwardID = a.EntryQuals.ENTRYQUALAWARDID,
AwardDate = a.EntryQuals.AWARDDATE
}).ToList();
This does some basic evaluations of the search criteria to see if it fits the purpose of the search. I.e. if they can search by name, date, or ID and IDs are numeric, we only search on an ID if the criteria was numeric. If it looked like a date, we search by date, otherwise we search by name. (and potentially other searchable strings)
If they can search for ID, FirstName, and BirthDate and enter one or more of those as separate entry fields (Search criteria page) then based on which entries they fill in you can either pass separate nullable parameters and do the above based on what parameters are passed, or pass a list of search values with something like an Enum for which value was searched for:
I.e. by parameters:
private void ByParameters(int? id = null, DateTime? birthDate = null, string name = null)
{
var whereClause = PredicateBuilder.False<StudentData>();
if(id.HasValue)
whereClause = whereClause.Or(x => x.SID == id.Value);
if(date.HasValue)
{
DateTime dateValue = date.Value.Date;
whereClause = whereClause.Or(x => x.BirthDate == dateValue);
}
if (!string.IsNullOrEmpty(name))
whereClause = whereClause.Or(x => x.FirstName.Contains(name));
// ....
}
If the number of parameters starts to get big, then a custom type can be created to encapsulate the individual null-able values. I.e.:
[Serializable]
public class SearchCriteria
{
public int? Id { get; set; }
public DateTime? BirthDate { get; set; }
public string Name { get; set; }
}
private void ByParameters(SearchCriteria criteria)
{
// ....
}
Or you can compose a more dynamic parameter list object with a criteria type and value but it starts getting more complex than it's probably worth.
You can't really do that in Linq, sine linq needs to know the the type of the field at compile time. A workaround would be something like
where (fieldVar=="SID" && a.SID == searchTxt) ||
(fieldVar=="FNAMES" && a.FNAMES== searchTxt) || ...
This will also alert you at compile time if you are doing an illegal comparison, eg. comparing a date to a string.

Choose one of three kind of bikes (Mountain Bikes, Touring Bikes, and Road Bikes) using LINQ

When I build a web app using MVC 4, it has three kind of bikes ("Mountain Bikes", "Touring Bikes", and "Road Bikes").
When one clicks on "Mountain Bikes" I want to get all models of "Mountain Bikes" and so on. What should I do to fix my code?
var listId = db.ProductSubcategories
.Where(pc => pc.ProductSubcategoryID == 1 || pc.ProductSubcategoryID == 2 || pc.ProductSubcategoryID == 3)
.Select(pc => pc.ProductSubcategoryID);
If I understand your question properly you need to actually know what the SubcategoryId is for the bike you are referring to e.g.
string selectedBike = "Mountain Bike";
var selectedBikeSubCategory = db.ProductSubcategories.FirstOrDefault(p => p.Name == selectedBike); // Assuming you have a name for sub categories
if (selectedBikeSubCategory != null)
{
var allMountainBikes = db.Products.Where(p => p.ProductSubcategoryID == selectedBikeSubCategoryId.ID); // will give you all products matching the selected sub category bike type
}
var criteria = new string[] {"1","2","3"};
var listId = from x in db.ProductSubcategories where criteria.Contains(x.ProductSubcategoryID);
Contains method is considered more readable

find property value from List<Postavke>() with linq

I have class "Postavke" and then i store it into
List<Postavke> postavke = new List<Postavke>();
Now i want to find some elemnt (property) from this List. I know "Name", "Surname" and i want to get "Address".
How to get "Adress" if i know "Name" and "Surname". All this are properties in "Postavke" class
whole class
public class Postavke
{
#region Properties
public string Name { get; set; }
public string Surname { get; set; }
public string Address { get; set; }
#endregion
#region Methods
public Postavke(string name, string surname, string address, string oznakaLokacije, string oznakaZapore)
{
Name = ean;
Surname = surname;
Address = address;
}
#endregion
}
You can query postavke for all results that contain the name and surname and put the results into a list. Putting the results into a list I find makes things easier to validate and handle unforseen items as from the looks of it it is possible that duplicate items could appear.
if the results must have all data within the list item then:
List<Postavke> results = new List<Postavke>();
var query1 = from a in postavke
where a.Name == searchName
&& a.Surname == searchSurname
select a;
results.AddRange(query1);
this list will have all the results that contain the exact name and surname.
If you just want the address then you can use:
List<string> results = new List<string>();
var query1 = from a in postavke
where a.Name == searchName
&& a.Surname == searchSurname
select a.Address;
results.AddRange(query1);
this will produce a list of addresses. From here you can then validate the list if you want by doing such things as checking to see how many items in the list there are so you know how you want to handle it etc.
If you want to use just either the name or the surname then you can remove the line that asks for one or the other.
If you end up with duplicates but the results are the same then you can change the line
results.AddRange(query1);
to
results.AddRange(query1.Distinct());
by using the Distinct() method it will sort the query and remove duplicates so the list will not be in the same order as it would if you didn't use it.
If you only wanted one result then it's worth validating it by checking how many items are in the list by using
results.Count
you could if it equals 1 carry on, otherwise throw up a message or some other way you may want to handle it. Other pieces of validation that are good is to set values ToLower() and Trim() during the search as to avoid actually changing the original text.
So taking the last query as an example:
List<string> results = new List<string>();
var query1 = from a in postavke
where a.Name.ToLower().Trim() == searchName.ToLower().Trim()
&& a.Surname.ToLower().Trim() == searchSurname.ToLower().Trim()
select a.Address;
results.AddRange(query1);
you can also filter the query with the where clause after you make the query by doing:
List<string> results = new List<string>();
var query1 = from a in postavke
select a.Address;
query1 = query1.Where(h => h.Name == searchName);
query1 = query1.Where(h => h.Surname == searchSurname);
results.AddRange(query1);
The 2 where clauses were split to show you can perform where clause at different points so you could have where clauses within if statements. you can combine the 2 into:
query1 = query1.Where(h => h.Name == searchName && h.Surname == searchSurname);
Hopefully this helps
This will work if you can be sure there's exactly one match
var address = poatavke.Where(p=>p.Name == name && p.Surname == surname).Single().Address;
If you don't know if there's no matches or exactly one you can do:
var posta = poatavke.Where(p=>p.Name == name && p.Surname == surname).SingleOrDefault()
var address = posta == null ? string.Empty : posta.Address;
if you don't know how many matches there's going to be but always want the first (or are using ET which doesn't understand Single())
var posta = poatavke.Where(p=>p.Name == name && p.Surname == surname).FirstOrDefault()
var address = posta == null ? string.Empty : posta.Address;

Categories

Resources