This is my linq query and I get lots of duplicates with school names.
so I created a regex function to trim the text:
public static string MyTrimmings(string str)
{
return Regex.Replace(str, #"^\s*$\n", string.Empty, RegexOptions.Multiline).TrimEnd();
}
the text gets trimed alright, however, the dropdown values are all duplicates! please help me eliminate duplicates, oh Linq joy!!
ViewBag.schools = new[]{new SelectListItem
{
Value = "",
Text = "All"
}}.Concat(
db.Schools.Where(x => (x.name != null)).OrderBy(o => o.name).ToList().Select(s => new SelectListItem
{
Value = MyTrimmings(s.name),
Text = MyTrimmings(s.name)
}).Distinct()
);
Distinct is poor, GroupBy for the win:
db.Schools.GroupBy(school => school.name).Select(grp => grp.First());
Assuming you have a School class you can write an IEqualityComparer
class SchoolComparer : IEqualityComparer<School>
{
public bool Equals(School x, School y)
{
//Check whether the compared objects reference the same data.
if (Object.ReferenceEquals(x, y)) return true;
//Check whether any of the compared objects is null.
if (Object.ReferenceEquals(x, null) || Object.ReferenceEquals(y, null))
return false;
//Check whether the school' properties are equal.
return x.Name == y.Name;
}
// If Equals() returns true for a pair of objects
// then GetHashCode() must return the same value for these objects.
public int GetHashCode(School school)
{
//Check whether the object is null
if (Object.ReferenceEquals(school, null)) return 0;
//Get hash code for the Name field if it is not null.
int hashSchoolName = school.Name == null ? 0 : school.Name.GetHashCode();
//Calculate the hash code for the school.
return hashSchoolName;
}
}
Then your linq query would look like this:
db.Schools.Where(x => x.name != null)
.OrderBy(o => o.name).ToList()
.Distinct(new SchoolComparer())
.Select(s => new SelectListItem
{
Value = MyTrimmings(s.name),
Text = MyTrimmings(s.name)
});
You could make your class implement the IEquatable<T> interface, so Distinct will know how to compare them. Like this (basic example):
public class SelectListItem : IEquatable<SelectListItem>
{
public string Value { get; set; }
public string Text { get; set; }
public bool Equals(SelectListItem other)
{
if (other == null)
{
return false;
}
return Value == other.Value && Text == other.Text;
}
public override int GetHashCode()
{
unchecked
{
int hash = 17;
if (Value != null)
{
hash = hash * 23 + Value.GetHashCode();
}
if (Text != null)
{
hash = hash * 23 + Text.GetHashCode();
}
return hash;
}
}
}
(GetHashCode taken fron John Skeet's answer here: https://stackoverflow.com/a/263416/249000)
Related
I've taken a stab at writing an incremental source generator; it is generating the correct source code, but it's not doing so incrementally. I feel like it has to be something wrong with my Initialize method or my custom return type (ClassInfo) not being cache friendly. I've never written an IEquatable either, so I really thing it has something to do with that.
ClassInfo
public readonly struct ClassInfo : IEquatable<ClassInfo>
{
public readonly string? Namespace { get; }
public readonly string Name { get; }
public readonly ImmutableArray<IPropertySymbol> PropertyNames { get; }
public ClassInfo(ITypeSymbol type)
{
Namespace = type.ContainingNamespace.IsGlobalNamespace ? null : type.ContainingNamespace.ToDisplayString();
Name = type.Name;
PropertyNames = GetPropertyNames(type);
}
private static ImmutableArray<IPropertySymbol> GetPropertyNames(ITypeSymbol type)
{
return type.GetMembers()
.Select(m =>
{
// Only properties
if (m is not IPropertySymbol prop || m.DeclaredAccessibility != Accessibility.Public)
return null;
// Without ignore attribute
if (GenHelper.IsPropsToStringIgnore(m))
return null;
return (IPropertySymbol)m;
//return SymbolEqualityComparer.Default.Equals(prop.Type, type) ? prop.Name : null;
})
.Where(m => m != null)!
.ToImmutableArray<IPropertySymbol>();
}
public override bool Equals(object? obj) => obj is ClassInfo other && Equals(other);
public bool Equals(ClassInfo other)
{
if (ReferenceEquals(null, other))
return false;
//if (ReferenceEquals(this, other))
// return true;
return Namespace == other.Namespace
&& Name == other.Name
&& PropertyNames.SequenceEqual(other.PropertyNames); // <-- Problem Line
}
public override int GetHashCode()
{
var hashCode = (Namespace != null ? Namespace.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ Name.GetHashCode();
hashCode = (hashCode * 397) ^ PropertyNames.GetHashCode(); // <-- Problem Line
return hashCode;
}
}
IncrementalGenerator.Initialize
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterPostInitializationOutput(ctx =>
{
ctx.AddSource("PropsToStringAttribute.g.cs", SourceText.From(AttributeTexts.PropsToStringAttribute, Encoding.UTF8));
ctx.AddSource("PropToStringAttribute.g.cs", SourceText.From(AttributeTexts.PropToStringAttribute, Encoding.UTF8));
ctx.AddSource("PropsToStringIgnoreAttribute.g.cs", SourceText.From(AttributeTexts.PropsToStringIgnoreAttribute, Encoding.UTF8));
});
var classProvider = context.SyntaxProvider
.CreateSyntaxProvider(
static (node, _) => node is ClassDeclarationSyntax { AttributeLists.Count: > 0 },
static (ctx, ct) => GetClassInfoOrNull(ctx, ct)
)
.Where(type => type is not null)
.Collect()
.SelectMany((classes, _) => classes.Distinct());
context.RegisterSourceOutput(classProvider, Generate);
}
GetClassInfoOrNull
internal static ClassInfo? GetClassInfoOrNull(GeneratorSyntaxContext context, CancellationToken cancellationToken)
{
// We know the node is a ClassDeclarationSyntax thanks to IsSyntaxTargetForGeneration
var classDeclarationSyntax = (ClassDeclarationSyntax)context.Node;
var type = ModelExtensions.GetDeclaredSymbol(context.SemanticModel, classDeclarationSyntax, cancellationToken) as ITypeSymbol;
return IsPropsToString(type) ? new ClassInfo(type!) : null;
}
IsPropsToString
public static bool IsPropsToString(ISymbol? type)
{
return type is not null &&
type.GetAttributes()
.Any(a => a.AttributeClass is
{
Name: ClassAttributeName,
ContainingNamespace:
{
Name: PTSNamespace,
ContainingNamespace.IsGlobalNamespace: true
}
});
}
IsPropsToStringIgnore
public static bool IsPropsToStringIgnore(ISymbol type)
{
return type is not null &&
type.GetAttributes()
.Any(a => a.AttributeClass is
{
Name: PropertyIgnoreAttributeName,
ContainingNamespace:
{
Name: PTSNamespace,
ContainingNamespace.IsGlobalNamespace: true
}
});
}
As a side note, I mostly followed this https://www.thinktecture.com/en/net/roslyn-source-generators-performance/
Edit 9/2/22
I have narrowed down the problem to two lines of code noted above in ClassInfo.Equals and ClassInfo.GetHashCode; the two lines that deal with equating the array of names. I commented out those two lines and started to get incremental code generation. However, I wasn't getting new code generation when properties changes (as espected), I instead had to change the name of the class(es) to get new code generated (again, as expected).
Edit 9/7/22
Added project to GitHub
Edit 9/8/22
I tried not using SequenceEquals to compare my PropertyNames array, but it didnt work.
public bool Equals(ClassInfo other)
{
if (PropertyNames.Count() != other.PropertyNames.Count())
return false;
int i = 0;
bool propIsEqual = true;
while (propIsEqual && i < PropertyNames.Count())
{
propIsEqual &= SymbolEqualityComparer.Default.Equals(PropertyNames[i], other.PropertyNames[i]);
i++;
}
return Namespace == other.Namespace
&& Name == other.Name
&& propIsEqual;
//PropertyNames.SequenceEqual(other.PropertyNames); // <-- Problem Line
}
You declared ClassInfo as struct, so it makes no sense to use ReferenceEquals in your Equals implementation. It will box the struct and always return other references.
public bool Equals(ClassInfo other)
{
return Namespace == other.Namespace
&& Name == other.Name
&& PropertyNames.SequenceEqual(other.PropertyNames);
}
PropertyNames.GetHashCode() only gets the hash code of the array object but does not include the array items; however it should, as Equals does it by calling SequenceEqual. The explicit IStructuralEquatable implementation of GetHashCode in ImmutableArray<T> does what we need.
public override int GetHashCode()
{
unchecked {
int hashCode = Namespace != null ? Namespace.GetHashCode() : 0;
hashCode = (hashCode * 397) ^ Name.GetHashCode();
hashCode = (hashCode * 397) ^
((IStructuralEquatable)PropertyNames)
.GetHashCode(EqualityComparer<IPropertySymbol>.Default);
return hashCode;
}
}
The IPropertySymbol implementation(s) must override the Equals and the GetHashCode methods as well. This is also important for SequenceEqual to work as expected.
Edit 22/09/08
We can remove uncertainties about mysterious equality comparers by comparing the properties ourselves:
public bool Equals(ClassInfo other)
{
// Length is faster than the extension method Count()
if (PropertyNames.Length != other.PropertyNames.Length) {
return false;
}
for (int i = 0; i < PropertyNames.Length; i++) {
if (PropertyNames[i].Name != other.PropertyNames[i].Name) {
return false; // We don't need to do the other tests.
}
}
return Namespace == other.Namespace && Name == other.Name;
}
Note: In your test project the "Counter = x" now gets incremented when we change a property name, but not if we add e.g. a comment.
Consequently, we do the same for GetHashCode (see also What is the best algorithm for overriding GetHashCode? and especially Jon Skeet's answer):
public override int GetHashCode()
{
unchecked {
int hash = 17;
hash = hash * 23 + (Namespace ?? "").GetHashCode();
hash = hash * 23 + Name.GetHashCode();
foreach (IPropertySymbol property in PropertyNames) {
hash = hash * 23 + property.Name.GetHashCode();
}
return hash;
}
}
This question already has answers here:
Distinct not working with LINQ to Objects [duplicate]
(11 answers)
Closed 4 years ago.
I am trying to extract distinct objects by their values to have the unique CurrencyISO I have in the .csv.
public List<CurrencyDetail> InitGSCheckComboCurrency()
{
var lines = File.ReadAllLines("Data/AccountsGSCheck.csv");
var data = (from l in lines.Skip(1)
let split = l.Split(',')
select new CurrencyDetail
{
ISOName = split[3],
ISOCode = split[3]
}).Distinct();
List<CurrencyDetail> lstSrv = new List<CurrencyDetail>();
lstSrv = data.ToList();
return lstSrv;
}
However, the distinct function does not work for this and I end up with duplications.
You would need to define the Equals and GetHashCode of CurrencyDetail to do what you want. Quick and dirty solution:
var data = (from l in lines.Skip(1)
let split = l.Split(',')
select new
{
ISOName = split[3],
ISOCode = split[3]
}).Distinct()
.Select(x => new CurrencyDetail
{
ISOName = x.ISOName,
ISOCode = x.ISOCode
};
Anonymous types (the first new { ... }) automatically define sensible Equals() and GetHashCode(). Normally I wouldn't do this, because you are creating objects to then discard them. For this reason it is a quick and dirty solution.
Note that you are using twice split[3]... an error?
Now, a fully equatable version of CurrencyDetail could be:
public class CurrencyDetail : IEquatable<CurrencyDetail>
{
public string ISOName { get; set; }
public string ISOCode { get; set; }
public override bool Equals(object obj)
{
// obj is object, so we can use its == operator
if (obj == null)
{
return false;
}
CurrencyDetail other = obj as CurrencyDetail;
if (object.ReferenceEquals(other, null))
{
return false;
}
return this.InnerEquals(other);
}
public bool Equals(CurrencyDetail other)
{
if (object.ReferenceEquals(other, null))
{
return false;
}
return this.InnerEquals(other);
}
private bool InnerEquals(CurrencyDetail other)
{
// Here we know that other != null;
if (object.ReferenceEquals(this, other))
{
return true;
}
return this.ISOName == other.ISOName && this.ISOCode == other.ISOCode;
}
public override int GetHashCode()
{
unchecked
{
// From http://stackoverflow.com/a/263416/613130
int hash = 17;
hash = hash * 23 + (this.ISOName != null ? this.ISOName.GetHashCode() : 0);
hash = hash * 23 + (this.ISOCode != null ? this.ISOCode.GetHashCode() : 0);
return hash;
}
}
}
With this you can use the Distinct() as used by your code.
I want an alphabetic sort with one exception.
There is a Group with a Name = "Public" and an ID = "0" that I want first.
(would rather use ID = 0)
After that then sort the rest by Name.
This does not return public first.
public IEnumerable<GroupAuthority> GroupAuthoritysSorted
{
get
{
return GroupAuthoritys.OrderBy(x => x.Group.Name);
}
}
What I want is:
return GroupAuthoritys.Where(x => x.ID == 0)
UNION
GroupAuthoritys.Where(x => x.ID > 0).OrderBy(x => x.Group.Name);
GroupAuthority has a public property Group and Group has Public properties ID and Name.
I used basically the accepted answer
using System.ComponentModel;
namespace SortCustom
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
TestSort();
}
private void TestSort()
{
List<CustomSort> LCS = new List<CustomSort>();
LCS.Add(new CustomSort(5, "sss"));
LCS.Add(new CustomSort(6, "xxx"));
LCS.Add(new CustomSort(4, "xxx"));
LCS.Add(new CustomSort(3, "aaa"));
LCS.Add(new CustomSort(7, "bbb"));
LCS.Add(new CustomSort(0, "pub"));
LCS.Add(new CustomSort(2, "eee"));
LCS.Add(new CustomSort(3, "www"));
foreach (CustomSort cs in LCS) System.Diagnostics.Debug.WriteLine(cs.Name);
LCS.Sort();
foreach (CustomSort cs in LCS) System.Diagnostics.Debug.WriteLine(cs.Name);
}
}
public class CustomSort : Object, INotifyPropertyChanged, IComparable<CustomSort>
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null) PropertyChanged(this, e);
}
private Int16 id;
private string name;
public Int16 ID { get { return id; } }
public String Name { get { return name; } }
public int CompareTo(CustomSort obj)
{
if (this.ID == 0) return -1;
if (obj == null) return 1;
if (obj is CustomSort)
{
CustomSort comp = (CustomSort)obj;
if (comp.ID == 0) return 1;
return string.Compare(this.Name, comp.Name, true);
}
else
{
return 1;
}
}
public override bool Equals(Object obj)
{
// Check for null values and compare run-time types.
if (obj == null) return false;
if (!(obj is CustomSort)) return false;
CustomSort comp = (CustomSort)obj;
return (comp.ID == this.ID);
}
public override int GetHashCode()
{
return (Int32)ID;
}
public CustomSort(Int16 ID, String Name)
{
id = ID;
name = Name;
}
}
}
You need to use a comparison function, they are functions that from two instances of your type return an integer that return 0 if both are equals, a negative value if the first is less than the second and a positive value if the first is greater than the second.
MSDN has a nice table that is easier to follow than text (StackOverflow still doesn't support tables in 2014)
IComparer<T>
Most sort methods accept a custom comparer implementation of type IComparer<T> you should create one encapsulating your custom rules for Group :
class GroupComparer : IComparer<Group>
{
public int Compare(Group a, Group b)
{
if (a != null && b != null && (a.Id == 0 || b.Id == 0))
{
if (a.Id == b.Id)
{
// Mandatory as some sort algorithms require Compare(a, b) and Compare(b, a) to be consistent
return 0;
}
return a.Id == 0 ? -1 : 1;
}
if (a == null || b == null)
{
if (ReferenceEquals(a, b))
{
return 0;
}
return a == null ? -1 : 1;
}
return Comparer<string>.Default.Compare(a.Name, b.Name);
}
}
Usage:
items.OrderBy(_ => _, new GroupAuthorityComparer());
IComparable<T>
If it is the only way to compare Group instances you should make it implement IComparable<T> so that no aditional code is needed if anyone want to sort your class :
class Group : IComparable<Group>
{
...
public int CompareTo(Group b)
{
if (b != null && (Id == 0 || b.Id == 0))
{
if (Id == b.Id)
{
// Mandatory as some sort algorithms require Compare(a, b) and Compare(b, a) to be consistent
return 0;
}
return Id == 0 ? -1 : 1;
}
return Comparer<string>.Default.Compare(Name, b.Name);
}
}
Usage:
items.OrderBy(_ => _.Group);
The choice between one way or the other should be done depending on where this specific comparer is used: Is it the main ordering for this type of item or just the ordering that should be used in one specific case, for example only in some administrative view.
You can even go one level up and provide an IComparable<GroupAuthority> implementation (It's easy once Group implement IComparable<Group>):
class GroupAuthority : IComparable<GroupAuthority>
{
...
public int CompareTo(GroupAuthority b)
{
return Comparer<Group>.Default.Compare(Group, b.Group);
}
}
Usage:
items.OrderBy(_ => _);
The advantage of the last one is that it will be used automatically, so code like: GroupAuthoritys.ToList().Sort() will do the correct thing out of the box.
You can try something like this
list.Sort((x, y) =>
{
if (x.Id == 0)
{
return -1;
}
if (y.Id == 0)
{
return 1;
}
return x.Group.Name.CompareTo(y.Group.Name);
});
Where list is List<T>.
This method takes advantage of custom sort option provided by List<T> using Comparison<T> delegate.
Basically what this method does is, it just adds special condition for comparison when Id, If it is zero it will return a value indicating the object is smaller which makes the object to come in top of the list. If not, it sorts the object using its Group.Name property in ascending order.
public IEnumerable<GroupAuthority> GroupAuthoritysSorted
{
get
{
return GroupAuthoritys.OrderBy(x => x.Group.ID == 0)
.ThenBy(x => x.Group.Name);
}
}
I have the following custom class deriving from Tuple:
public class CustomTuple : Tuple<List<string>, DateTime?>
{
public CustomTuple(IEnumerable<string> strings, DateTime? time)
: base(strings.OrderBy(x => x).ToList(), time)
{
}
}
and a HashSet<CustomTuple>. The problem is that when I add items to the set, they are not recognised as duplicates. i.e. this outputs 2, but it should output 1:
void Main()
{
HashSet<CustomTuple> set = new HashSet<CustomTuple>();
var a = new CustomTuple(new List<string>(), new DateTime?());
var b = new CustomTuple(new List<string>(), new DateTime?());
set.Add(a);
set.Add(b);
Console.Write(set.Count); // Outputs 2
}
How can I override the Equals and GetHashCode methods to cause this code to output a set count of 1?
You should override GetHashCode and Equals virtual methods defined in System.Object class.
Please remember that:
If two objects are logically "equal" then they MUST have the same hash code!
If two objects have the same hashcode, then it is not mandatory to have your objects equal.
Also, i've noticed an architectural problem in your code:
List is a mutable type but overriding Equals and GetHashCode usually makes your class logically to behave like a value type. So having "Item1" a mutable type and behaving like a value type is very dangerous. I suggest replacing your List with a ReadOnlyCollection . Then you would have to make a method that checks whether two ReadOnlyCollections are Equal.
For the GetHashCode () method, just compose a string from all string items found in Item1 then append a string that represents the Hash code for datetime then finally call on the concatenated result the "GetHashCode ()" overrided on string method. So normally you would have:
override int GetHashCode () {
return (GetHashCodeForList (Item1) + (Item2 ?? DateTime.MinValue).GetHashCode ()).GetHashCode ();
}
And the GetHashCodeForList method would be something like this:
private string GetHashCodeForList (IEnumerable <string> lst) {
if (lst == null) return string.Empty;
StringBuilder sb = new StringBuilder ();
foreach (var item in lst) {
sb.Append (item);
}
return sb.ToString ();
}
Final note: You could cache the GetHashCode result since it is relative expensive to get and your entire class would became immutable (if you replace List with a readonly collection).
A HashSet<T> will first call GetHashCode, so you need to work on that first. For an implementation, see this answer: https://stackoverflow.com/a/263416/1250301
So a simple, naive, implementation might look like this:
public override int GetHashCode()
{
unchecked
{
int hash = 17;
hash = hash * 23 + this.Item2.GetHashCode();
foreach (var s in this.Item1)
{
hash = hash * 23 + s.GetHashCode();
}
return hash;
}
}
However, if your lists are long, then this might not be efficient enough. So you'll have to decide where to compromise depending on how tolerant you are of collisions.
If the result of GetHashCode for two items are the same, then, and only then, will it call Equals. An implementation of Equals is going to need to compare the items in the list. Something like this:
public override bool Equals(object o1)
{
var o = o1 as CustomTuple;
if (o == null)
{
return false;
}
if (Item2 != o.Item2)
{
return false;
}
if (Item1.Count() != o.Item1.Count())
{
return false;
}
for (int i=0; i < Item1.Count(); i++)
{
if (Item1[i] != o.Item1[i])
{
return false;
}
}
return true;
}
Note that we check the date (Item2) first, because that's cheap. If the date isn't the same, we don't bother with anything else. Next we check the Count on both collections (Item1). If they don't match, there's no point iterating the collections. Then we loop through both collections and compare each item. Once we find one that doesn't match, we return false because there is no point continuing to look.
As pointed out in George's answer, you also have the problem that your list is mutable, which will cause problems with your HashSet, for example:
var a = new CustomTuple(new List<string>() {"foo"} , new DateTime?());
var b = new CustomTuple(new List<string>(), new DateTime?());
set.Add(a);
set.Add(b);
// Hashset now has two entries
((List<string>)a.Item1).Add("foo");
// Hashset still has two entries, but they are now identical.
To solve that, you need to force your IEnumerable<string> to be readonly. You could do something like:
public class CustomTuple : Tuple<IReadOnlyList<string>, DateTime?>
{
public CustomTuple(IEnumerable<string> strings, DateTime? time)
: base(strings.OrderBy(x => x).ToList().AsReadOnly(), time)
{
}
public override bool Equals(object o1)
{
// as above
}
public override int GetHashCode()
{
// as above
}
}
This is is what I went for, which outputs 1 as desired:
private class CustomTuple : Tuple<List<string>, DateTime?>
{
public CustomTuple(IEnumerable<string> strings, DateTime? time)
: base(strings.OrderBy(x => x).ToList(), time)
{
}
public override bool Equals(object obj)
{
if (obj == null || GetType() != obj.GetType())
{
return false;
}
var that = (CustomTuple) obj;
if (Item1 == null && that.Item1 != null || Item1 != null && that.Item1 == null) return false;
if (Item2 == null && that.Item2 != null || Item2 != null && that.Item2 == null) return false;
if (!Item2.Equals(that.Item2)) return false;
if (that.Item1.Count != Item1.Count) return false;
for (int i = 0; i < Item1.Count; i++)
{
if (!Item1[i].Equals(that.Item1[i])) return false;
}
return true;
}
public override int GetHashCode()
{
int hash = 17;
hash = hash*23 + Item2.GetHashCode();
return Item1.Aggregate(hash, (current, s) => current*23 + s.GetHashCode());
}
}
I'm looking to group a list based on a list within that list itself, given the following data structure:
public class AccDocumentItem
{
public string AccountId {get;set;}
public List<AccDocumentItemDetail> DocumentItemDetails {get;set;}
}
And
public class AccDocumentItemDetail
{
public int LevelId {get;set;}
public int DetailAccountId {get;set;}
}
I now have a List<AccDocumentItem> comprised of 15 items, each of those items has a list with variable number of AccDocumentItemDetail's, the problem is that there may be AccDocumentItems that have identical AccDocumentItemDetails, so I need to group my List<AccDocumentItem> by it's AccDocumentItemDetail list.
To make it clearer, suppose the first 3 (of the 15) elements within my List<AccDocumentItem> list are:
1:{
AccountId: "7102",
DocumentItemDetails:[{4,40001},{5,40003}]
}
2:{
AccountId: "7102",
DocumentItemDetails:[{4,40001},{6,83003},{7,23423}]
}
3:{
AccountId: "7102",
DocumentItemDetails:[{4,40001},{5,40003}]
}
How can I group my List<AccDocumentItem> by it's DocumentItemDetails list such that row 1 and 3 are in their own group, and row 2 is in another group?
Thanks.
You could group by the comma separated string of detail-ID's:
var query = documentItemList
.GroupBy(aci => new{
aci.AccountId,
detailIDs = string.Join(",", aci.DocumentItemDetails
.OrderBy(did => did.DetailAccountId)
.Select(did => did.DetailAccountId))
});
Another, more ( elegant,efficient,maintainable ) approach is to create a custom IEqualityComparer<AccDocumentItem>:
public class AccDocumentItemComparer : IEqualityComparer<AccDocumentItem>
{
public bool Equals(AccDocumentItem x, AccDocumentItem y)
{
if (x == null || y == null)
return false;
if (object.ReferenceEquals(x, y))
return true;
if (x.AccountId != y.AccountId)
return false;
return x.DocumentItemDetails
.Select(d => d.DetailAccountId).OrderBy(i => i)
.SequenceEqual(y.DocumentItemDetails
.Select(d => d.DetailAccountId).OrderBy(i => i));
}
public int GetHashCode(AccDocumentItem obj)
{
if (obj == null) return int.MinValue;
int hash = obj.AccountId.GetHashCode();
if (obj.DocumentItemDetails == null)
return hash;
int detailHash = 0;
unchecked
{
foreach (var detID in obj.DocumentItemDetails.Select(d => d.DetailAccountId))
detailHash = detailHash * 23 + detID;
}
return hash + detailHash;
}
}
Now you can use it for GroupBy:
var query = documentItemList.GroupBy(aci => aci, new AccDocumentItemComparer());
You can use that for many other Linq extension methods like Enumerable.Join etc. also.