GroupBy on complex object (e.g. ListT)
To get objects to work with many of LINQ's operators, such as GroupBy
or Distinct
, you must either implement GetHashCode
& Equals
, or you must provide a custom comparer.
In your case, with a property as a list you probably need a comparer, unless you made the list read only.
Try this comparer:
public class SampleObjectComparer : IEqualityComparer<SampleObject>
{
public bool Equals(SampleObject x, SampleObject y)
{
return x.Id == y.Id && x.Events.SequenceEqual(y.Events);
}
public int GetHashCode(SampleObject x)
{
return x.Id.GetHashCode() ^ x.Events.Aggregate(0, (a, y) => a ^ y.GetHashCode());
}
}
Now this code works:
var items = new List<SampleObject>()
{
new SampleObject() { Id = "Id", Events = new List<string>() { "ExampleEvent"} },
new SampleObject() { Id = "Id", Events = new List<string>() { "ExampleEvent" } }
};
var comparer = new SampleObjectComparer();
var duplicates = items.GroupBy(x => x, comparer)
.Where(g => g.Count() > 1)
.Select(g => g.Key)
.ToList();
Linq Group by Complex Object (E.G list element of object)
1 ) I've created my classes to meet json,
public class MyTable {
public string TableNumber { get; set; }
public string TableId { get; set; }
public List<TableMenu> tableMenuItems { get; set; }
public int TotalPrice { get; set; }
}
public class TableMenu {
public string Id { get; set; }
public string Name { get; set; }
public List<TableMenuItemCondiment> tableMenuItemCondiment { get; set; }
public int Price { get; set; }
public int Count { get; set; }
}
public class TableMenuItemCondiment {
public string Id { get; set; }
public string Name { get; set; }
public int Price { get; set; }
}
2 ) Then i've deserialized object by NewtonSoft,
MyTable myTable = JsonConvert.DeserializeObject<MyTable>(json);
3 ) Converted object as,
MyTable result = new MyTable() {
TableNumber = myTable.TableNumber,
TableId = myTable.TableId,
tableMenuItems = myTable.tableMenuItems.GroupBy(x => x.Id).Select(x => new TableMenu() {
Id = x.Key,
Name = x.First()?.Name,
tableMenuItemCondiment = x.SelectMany(a=>a.tableMenuItemCondiment).GroupBy(z => z.Name).Select(z => new TableMenuItemCondiment() {
Id = z.First()?.Id,
Name = z.Key,
Price = z.Sum(t => t.Price)
}).ToList(),
Price = x.Sum(t => t.Price),
Count = x.Sum(t => t.Count)
}).ToList(),
TotalPrice = myTable.TotalPrice
};
4 ) Finally serialized object to see whether it gives the response as you want,
string output = JsonConvert.SerializeObject(result);
5 ) Print the output,
Console.WriteLine(output);
Console.ReadKey();
Output:
{ "TableNumber":"3",
"TableId":"81872d39-9480-4d2d-abfc-b8e4f33a43b6",
"tableMenuItems":[
{
"Id":"4664a2d3-c0af-443d-8af5-2bd21e71838b",
"Name":"Bonfile",
"tableMenuItemCondiment":[
{
"Id":"9b1f01a0-0313-46b6-b7f1-003c0e846136",
"Name":"Kekikkli",
"Price":0
},
{
"Id":"38a9cce6-f20c-4f78-b6c9-c15e79ecc8f1",
"Name":"mayonez",
"Price":0
},
{
"Id":"cb3a7811-668b-4e45-bce3-e6b2b13af9e1",
"Name":"Rare",
"Price":0
}
],
"Price":90,
"Count":2
}
],
"TotalPrice":90 }
How to .GroupBy() by Id and by list property?
A typical way to compare two lists is to use the System.Linq
exension method, SequenceEquals
. This method returns true if both lists contain the same items, in the same order.
In order to make this work with an IEnumerable<EvaluatedTag>
, we need to have a way to compare instances of the EvaluatedTag
class for equality (determining if two items are the same) and for sorting (since the lists need to have their items in the same order).
To do this, we can override Equals
and GetHashCode
and implement IComparable<EvaluatedTag>
(and might as well do IEquatable<EvaluatedTag>
for completeness):
public class EvaluatedTag : IEquatable<EvaluatedTag>, IComparable<EvaluatedTag>
{
public string Id { get; set; }
public string Name { get; set; }
public int CompareTo(EvaluatedTag other)
{
if (other == null) return -1;
var result = string.CompareOrdinal(Id, other.Id);
return result == 0 ? string.CompareOrdinal(Name, other.Name) : result;
}
public bool Equals(EvaluatedTag other)
{
return other != null &&
string.Equals(other.Id, Id) &&
string.Equals(other.Name, Name);
}
public override bool Equals(object obj)
{
return Equals(obj as EvaluatedTag);
}
public override int GetHashCode()
{
return Id.GetHashCode() * 17 +
Name.GetHashCode() * 17;
}
}
Now we can use this in the custom comparer you have in your question, for sorting and comparing the EvaluatedTags
:
public class AlertEvaluationComparer : IEqualityComparer<AlertEvaluation>
{
// Return true if the AlertIds are equal, and the EvaluatedTags
// contain the same items (call OrderBy to ensure they're in
// the same order before calling SequenceEqual).
public bool Equals(AlertEvaluation x, AlertEvaluation y)
{
if (x == null) return y == null;
if (y == null) return false;
if (!string.Equals(x.AlertId, y.AlertId)) return false;
if (x.EvaluatedTags == null) return y.EvaluatedTags == null;
if (y.EvaluatedTags == null) return false;
return x.EvaluatedTags.OrderBy(et => et)
.SequenceEqual(y.EvaluatedTags.OrderBy(et => et));
}
// Use the same properties in GetHashCode that were used in Equals
public int GetHashCode(AlertEvaluation obj)
{
return obj.AlertId?.GetHashCode() ?? 0 * 17 +
obj.EvaluatedTags?.Sum(et => et.GetHashCode() * 17) ?? 0;
}
}
And finally we can pass your AlertEvaluationComparer
to the GroupBy
method to group our items:
var evaluationsGroupedAndOrdered = evaluations
.GroupBy(ae => ae, new AlertEvaluationComparer())
.OrderBy(group => group.Key.EvaluationDate)
.ToList();
Linq Group by Complex Type of List
You could create two IEqualityComparer<T>
classes here. One for comparing if two CondimentDto
objects are equal and one for comparing if two List<CondimentDto>
are equal. We pass the CondimentComparer
to SequenceEqual()
to determine if two lists are equal, and we pass ItemDtoComparer
to GroupBy
to group ItemDto
objects from the main items
list. We have to also create our own Equals()
and GetHashCode()
to determine the equality.
public class CondimentComparer : IEqualityComparer<CondimentDto>
{
public bool Equals(CondimentDto x, CondimentDto y)
{
// If all the properties match, then its equal
return x.PosItemId == y.PosItemId &&
x.ProductName == y.ProductName &&
x.OriginalPrice == y.OriginalPrice;
}
// Get all hashcodes from object properties using XOR
// Might be better ways to do this
public int GetHashCode(CondimentDto obj)
{
return obj.PosItemId.GetHashCode() ^
obj.ProductName.GetHashCode() ^
obj.OriginalPrice.GetHashCode();
}
}
public class ItemDtoComparer : IEqualityComparer<ItemDto>
{
public bool Equals(ItemDto x, ItemDto y)
{
// If two condiments are null, then just compare ItemId
if (x.Condiments == null && y.Condiments == null)
{
return x.ItemId == y.ItemId;
}
// If one is null and the other is not, can't possiblly be equal
else if (x.Condiments == null && y.Condiments != null ||
x.Condiments != null && y.Condiments == null)
{
return false;
}
// If we get here we have two normal objects
// Compare two objects ItemId and lists of condiments
return x.ItemId == y.ItemId &&
x.Condiments.SequenceEqual(y.Condiments, new CondimentComparer());
}
public int GetHashCode(ItemDto obj)
{
// If condiments list is null, just get the ItemId hashcode
if (obj.Condiments == null)
{
return obj.ItemId.GetHashCode();
}
// Otherwise do the XOR of ItemId and the aggregated XOR of the condiment list properties
return obj.ItemId.GetHashCode() ^ obj.Condiments.Aggregate(0, (a, c) => a ^ c.PosItemId.GetHashCode() ^ c.ProductName.GetHashCode() ^ c.OriginalPrice.GetHashCode());
}
}
Then you can pass ItemDtoComparer
to Groupby
:
var result = items
.GroupBy(item => item, new ItemDtoComparer()) // Pass ItemDtoComparer here
.Select(grp => new ItemDto
{
ItemId = grp.Key.ItemId,
ProductName = grp.Key.ProductName,
Condiments = grp.Key.Condiments,
Count = grp.Count()
})
.ToList();
Demo on dotnetfiddle.net
Note: The above GetHashCode()
implementations are XORing hashcodes. This is considered to not be best practice, and following the suggestions in this answer offer better alternatives to avoiding hash collisions. I just chose this method as its easy to remember and demo.
GroupBy() not grouping correctly on ListT property
When you include a list property in GroupBy
you end up with list comparisons deep inside GroupBy
implementation, which is usually not what you want.
You could solve this by implementing a custom IEqualityComparer
, but then you would have to define a named type for the group key, because IEqualityComparer
cannot be defined for an anonymous type. Another approach is to group by a composite string
key, like this:
.GroupBy(sd => new {
sd.Shift.Id
, string.Join(
"|"
, sd.DayFunctions
.OrderBy(f => f.ScheduleDayId).ThenBy(f => f.FunctionId)
.Select(f => $"{f.ScheduleDayId}:{f.FunctionId}")
)
})
Using LINQ GroupBy to group by reference objects instead of value objects
To accomplish this task, you can override Equals() and GetHashCode() methods for your classes:
public class Room {
public string Name;
public string Foo;
public override bool Equals(object obj)
{
Room other = obj as Room;
if (other == null) return false;
return this.Name == other.Name && this.Foo == other.Foo;
}
public override int GetHashCode()
{
return (Name.GetHashCode() ^ Foo.GetHashCode()).GetHashCode();
}
}
Take a look here for more complicated example
LINQ Group By into a Dictionary Object
Dictionary<string, List<CustomObject>> myDictionary = ListOfCustomObjects
.GroupBy(o => o.PropertyName)
.ToDictionary(g => g.Key, g => g.ToList());
GroupBy on multiple properties with complex type
Enumerable.GroupBy
allows you to pass custom equality comparer for keys. But in your case key is not an Address
object - it's an anonymous object containing three properties - SiteRefNum
, SiteRefName
and Address
. Of course passing AddressComparer
to compare such keys will cause an error.
And your first problem was using complex object as key property. If you don't override Equals
and GetHashCode
methods for Address
objects, then all addresses will be compared by reference. Which is of course different for each address instance. You can provide Equals
and GetHashCode
implementations to compare addresses.
Or you can modify your query to use address string for grouping:
var aggregated =
from s in sitesWithLive
group s by new {
s.SiteRefNum,
s.SiteRefName,
Address = s.Address.ToString() // here we group by string
} into g
select new Site
{
SiteRefNum = g.Key.SiteRefNum,
SiteRefName = g.Key.SiteRefName,
Address = g.First().Address, // here we just get first address object
ContractLive = g.Max(x => x.ContractLive)
};
You can use method syntax for query, but I find declarative query syntax more readable :)
How do I use itertools.groupby()?
IMPORTANT NOTE: You have to sort your data first.
The part I didn't get is that in the example construction
groups = []
uniquekeys = []
for k, g in groupby(data, keyfunc):
groups.append(list(g)) # Store group iterator as a list
uniquekeys.append(k)
k
is the current grouping key, and g
is an iterator that you can use to iterate over the group defined by that grouping key. In other words, the groupby
iterator itself returns iterators.
Here's an example of that, using clearer variable names:
from itertools import groupby
things = [("animal", "bear"), ("animal", "duck"), ("plant", "cactus"), ("vehicle", "speed boat"), ("vehicle", "school bus")]
for key, group in groupby(things, lambda x: x[0]):
for thing in group:
print("A %s is a %s." % (thing[1], key))
print("")
This will give you the output:
A bear is a animal.
A duck is a animal.A cactus is a plant.
A speed boat is a vehicle.
A school bus is a vehicle.
In this example, things
is a list of tuples where the first item in each tuple is the group the second item belongs to.
The groupby()
function takes two arguments: (1) the data to group and (2) the function to group it with.
Here, lambda x: x[0]
tells groupby()
to use the first item in each tuple as the grouping key.
In the above for
statement, groupby
returns three (key, group iterator) pairs - once for each unique key. You can use the returned iterator to iterate over each individual item in that group.
Here's a slightly different example with the same data, using a list comprehension:
for key, group in groupby(things, lambda x: x[0]):
listOfThings = " and ".join([thing[1] for thing in group])
print(key + "s: " + listOfThings + ".")
This will give you the output:
animals: bear and duck.
plants: cactus.
vehicles: speed boat and school bus.
Related Topics
Reading from a Text File in C#
Trying to Debug Windows Store App from Dump Files
Horizontal Scroll for Stackpanel Doesn't Work
Using a 32Bit or 64Bit Dll in C# Dllimport
How to Add New Line into Txt File
How Can a C++ Windows Dll Be Merged into a C# Application Exe
How to Retrieve Disk Information in C#
Parsing Performance (If, Tryparse, Try-Catch)
Xml Validation Using Xsd Schema
Wait for a While Without Blocking Main Thread
Jquery Ajax Call to an ASP.NET Webmethod
An Error Occurred During Report Processing. -Rldc Reporting in ASP.NET MVC