I am grouping a collection, by multiple columns. It is easy to group them if they are on this same level of nesting:
var groupedAirProducts = airproductsPerClient.GroupBy(ac => new
{
ac.AirProduct.PassengerNameRecord,
ac.AirProduct.Flights.First().MarketingAirlineCode,
ac.AirProduct.Flights.First().FlightNo,
ac.AirProduct.Flights.First().DepartureDate
})
The problem is that I don't want only to group by a first flight, but I want to include all the flights. What I would like to do is something like:
var groupedAirProducts= airproductsPerClient.GroupBy(ac =>
{
ac.AirProduct.PassengerNameRecord,
foreach(var flight in ac.AirProduct.Flights){
flight.MarketingAirlineCode,
flight.FlightNo,
flight.DepartureDate
}
})
Please note that the code above is just an illustration of an idea. It is not a working/proper code. The only way I know how to do it is pretty ugly and complex. I was wondering if there is a nice way to do it using LINQ, or a simple way to tackle this problem.
Edit:
To explain more of what is expected. As a result of grouping I want to have collections of airProducts that share same PassengerNameRecord, and that flights that belong to a given air product share same MarketingAirlineCode, FlightNo, DepartureDate. I know how to implement it by flattering a collection of PassengerNameRecord, so the Flights are included in it, grouping it, so I have a groups of air products that share grouped properties. And then rebuilding the flattered structure. I was hoping that there is a way to either add groupBy properties by iterating thru some collection, or there is a way to merge grouped collection - if there is a way to have a collection grouped by PassengerNameRecord, and collection grouped by Flight properties and merging them together, unfortunately I doubt that there is an easy way to do such merge.
You can implement a custom IEqualityComparer<AirClient> for GroupBy(or other LINQ methods).
You can implement it in the following way:
public class AirClientComparer : IEqualityComparer<AirClient>
{
public bool Equals(AirClient lhs, AirClient rhs)
{
if (lhs == null || rhs == null) return false;
if(object.ReferenceEquals(lhs, rhs)) return true;
if(lhs.PassengerNameRecord != rhs.PassengerNameRecord) return false;
if(object.ReferenceEquals(lhs.AirProduct, rhs.AirProduct)) return true;
if(lhs.AirProduct == null || rhs.AirProduct == null) return false;
if(object.ReferenceEquals(lhs.AirProduct.Flights , rhs.AirProduct.Flights )) return true;
if(lhs.AirProduct.Flights.Count != rhs.AirProduct.Flights.Count) return false;
if(lhs.AirProduct.Flights.Count == 0 && rhs.AirProduct.Flights.Count == 0) return true;
return lhs.AirProduct.Flights.All(f =>
rhs.AirProduct.Flights.Any(f2 =>
f.MarketingAirlineCode == f2.MarketingAirlineCode
&& f.FlightNo == f2.FlightNo
&& f.DepartureDate == f2.DepartureDate));
}
public int GetHashCode(AirClient obj)
{
if(obj.AirProduct == null) return 0;
int hash = obj.AirProduct.PassengerNameRecord == null
? 17 : 17 * obj.AirProduct.PassengerNameRecord.GetHashCode();
unchecked
{
foreach(var flight in obj.AirProduct.Flights)
{
hash = (hash * 31) + flight.MarketingAirlineCode == null ? 0 : flight.MarketingAirlineCode.GetHashCode();
hash = (hash * 31) + flight.FlightNo == null ? 0 : flight.FlightNo.GetHashCode();
hash = (hash * 31) + flight.DepartureDate.GetHashCode();
}
}
return hash;
}
}
Now you can use it for example in GroupBy:
var groupedAirProducts = airproductsPerClient.GroupBy(ac => new AirClientComparer());
In such cases, you need to start with the inner table.
The inner table also needs a reference to the outer table, such like you have a
ICollection<Flight> Flights in AirProduct
you also need a Airproduct Member inside of Flight.
Then group your flights like this (pseudo code)
AirProduct.Flights.groupby( flight => new{ flight.MarketingAirlineCode,
flight.FlightNo,
flight.DepartureDate,
flight.product.PassengerNameRecord
});
Related
so I've got two lists of objects, objects have multiple fields, but I'd like to distinct them basing on only two of them.
To give you the picture, object KeyAndValue consists of fields Key and Tag, so:
list1 = { obj1(key=1,tag=A), obj2(key=2,tag=A) }
list2 = { obj3(key=1,tag=A), obj4(key=2,tag=B) }
I'm currently using:
list1.Where(x => !list2.Any(y => y.key == x.key)).ToList();
The correct result is: obj1, obj2 and obj4 as obj3 has the same key and tag as obj1
What I'm trying to accomplish is to speed this process up as with a lot of objects it takes much too long. I found that custom IEqualityComparer could help here, so I've written my own basing on MS Specification
It looks like this:
class KeyComparer : IEqualityComparer<KeyAndValue>
{
public bool Equals(KeyAndValue x, KeyAndValue y)
{
if (Object.ReferenceEquals(x, y))
return true;
if (Object.ReferenceEquals(x, null) || Object.ReferenceEquals(y, null))
return false;
return x.key == y.key && x.tag == y.tag;
}
public int GetHashCode(KeyAndValue keyAndValue)
{
if (Object.ReferenceEquals(keyAndValue, null))
return 0;
int hashKeyAndValueKey = keyAndValue.key == null ? 0 : keyAndValue.key.GetHashCode();
int hashKeyAndValueTag = keyAndValue.tag == null ? 0 : keyAndValue.tag.GetHashCode();
return hashKeyAndValueKey ^ hashKeyAndValueTag;
}
}
And I use it like this:
list1.Except(list2, new KeyComparer()).ToList();
Unfortunately it does only remove duplicates from list2. It seems that it does not even touch list1 and I do not know if it's the fault of my custom comparer, the way I use it or maybe I should use another method. I've been looking through other questions but could not find a working answer (or at least one that I'd actually know how to implement properly).
I think you don't need Except. You want to have distinct values of both?
var distinctValues = list1.Union(list2).Distinct();
You need to either implement GetHashCode/Equals in KeyAndValue or use a comparer to compare the objects by key and value.
(Old stuff bellow)
Not sure whether I understand the question correctly. Could it be that you didn't recognize that Except create a new IEnumerable and ToList a new List?
try:
var list1AdditionalItems = list1.Except(list2, new KeyComparer()).ToList();
and probably also:
var list2AdditionalItems = list2.Except(list1, new KeyComparer()).ToList();
Another observation. in this code:
list1.Where(x => !list2.Any(y => y.key == x.key)).ToList();
You only check the key. If you want this behaviour, you should write the comparer accordingly:
class KeyComparer : IEqualityComparer<KeyAndValue>
{
public bool Equals(KeyAndValue x, KeyAndValue y)
{
if (ReferenceEquals(x, y))
return true;
if (ReferenceEquals(x, null) || ReferenceEquals(y, null))
return false;
return x.key == y.key;
}
public int GetHashCode(KeyAndValue keyAndValue)
{
if (ReferenceEquals(keyAndValue, null))
return 0;
return keyAndValue.key == null ? 0 : keyAndValue.key.GetHashCode();
}
}
Last but not least: When you need performance, consider using a dictionary instead.
As Stefan Steinegger (whom I thank grately for the effort and time spent) mentioned in the first comment that neither of my methods return obj4 I found an issue and decided to implement totally different approach. Now my KeyAndValue class has also an int Hash field, when a constructor is called Hash field is filled with key.GetHashCode() ^ tag.GetHashCode(). It simplified the comparison as now I'm first combining list1 with list2 and then sending it through: CombinedList.GroupBy(x => x.Hash).Select(y => y.First()).ToList(); Results seem to be correct :)
I have a list of these objects:
public class seat
{
public String id, tooltip;
public String Section, Row, Number;
public Boolean Taken;
}
I would like to build a function to search the elements of the class. However, I will not always be searching for all of the elements.
I know I could do this with a loop, and some if-statements. Saying something along the lines of
public ArrayList searchArray(String section, String row, String number)
{
ArrayList searched = new ArrayList();
foreach(seat item in seats)//seats is a list of the seat class
{
if(section!="" && row!=""&& id!="")
{
if(item.Section==section && item.Row==row &&item.id==id)
searched.Add(item);
}
else if(section!="" && row!="")
{
if(item.Section==section && item.Row==row)
searched.Add(item);
}
else if(row!="")
{
if(item.Row==row)
searched.Add(item);
}
/////Continue this for all the other combinations of searching
}
return searched;
}
I could also to it several loops like
if(Section!="")
foreach(seat item in seats)
if(item.Section==section)
searched.Add(item);
seats = searched;
search.Clear();
if(id!="")
foreach(seat item in seats)
if(item.id==id)
searched.Add(item);
seats = searched;
search.Clear();
if(row!="")
foreach(seat item in seats)
if(item.Row==row)
searched.Add(item);
So first one is tedious and requires a lot of ugly code.
The second is a little better, but requires I go through the list more than once. More specifically it requires me to go through the list for each parameter I am looking for.
Is there a way I can do this where you just add the parameters you want to look for and then search. Sort of like you generate an sql query to search.
Less important, but would be amazing if it could work, would be to even allow ranges for the search. Like id>2 && id<12
This is where IEnumerable<> is your friend!
IEnumerable<seat> query = seats.AsEnumerable();
if(!string.IsNullOrEmpty(section))
query = query.Where(s => s.Section == section);
if(!string.IsNullOrEmpty(row))
query = query.Where(s => s.Row == row);
if(!string.IsNullOrEmpty(id))
query = query.Where(s => s.Id == id);
List<seat> results = query.ToList(); // deferred execution
ArrayList searched = new ArrayList(
seats.Where(c => c.Section == section && !string.IsNullOrEmpty(section))
.Where(c => c.Row == row && !string.IsNullOrEmpty(row))
.Where(c => c.id == id && !string.IsNullOrEmpty(id)).ToList());
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
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;
}
I have a DB table that looks similar to this.
ID | Status | Type
etc...etc
I am using linq to try and discern distinct Statuses from this collection like so
results = ctx.Status.Distinct(new StatusComparer()).ToList();
but this returns all statuses, I used the following Link to construct the Comparer below, I found that suggestion in another StackOverflow Post
public class StatusComparer : IEqualityComparer<Status>
{
public bool Equals(Status x, Status y)
{
// Check whether the compared objects reference the same data.
if (ReferenceEquals(x, y))
{
return true;
}
// Check whether any of the compared objects is null.
if (ReferenceEquals(x, null) || ReferenceEquals(y, null))
{
return false;
}
// Check whether the status' properties are equal.
return x.StatusDescription == y.StatusDescription && x.Type == y.Type && x.StatusID == y.StatusID;
}
public int GetHashCode(Status status)
{
// Get hash code for the Name field if it is not null.
var hashStatusId = status.StatusID.GetHashCode();
// Get hash code for the Code field.
var hashStatusDescription = status.StatusDescription.GetHashCode();
var hashStatusType = status.Type.GetHashCode();
// Calculate the hash code for the product.
return hashStatusId ^ hashStatusDescription ^ hashStatusType;
}
}
}
My problem is as follows early on we had a system that worked fine, so well in fact they wanted another system using the same Database so we plumbed it in. The search has an advanced options with several filters one of them being Status but as you can see from the above (loose) DB structure statuses have different types but similar text. I need to be able to select via Linq the whole status by the distinct text. all help would be greatly appreciated.
have also tried
results = (from s in context.Status group s by s.StatusDescription into g select g.First()).ToList();
this also failed with a System.NotSupportedException
To select all distinct statuses:
ctx.Status.Select(s => new { s.StatusDescription, s.Type }).Distinct();