您可以使用以下冗长的LINQ查询来做到这一点:
var result = flatManyToMany
.GroupBy(f1 => (f1.BookTitle,f1.BookPages))
.Select(g1 => (bookInfo: g1.Key,readers:
g1.Select(f2 => new Reader { Name= f2.ReaderName,Age= f2.ReaderAge }),readerKey:
String.Join("|",g1.Select(f3 => $"{f3.ReaderName}{f3.ReaderAge}"))))
.GroupBy(a1 => a1.readerKey)
.Select(g2 => new ResultDoubleList {
Books = g2.Select(a2 => new Book {
Title = a2.bookInfo.BookTitle,Pages = a2.bookInfo.BookPages
}
).ToList(),Readers = g2.First().readers.ToList() // Any will do,since they have the same readers
})
.ToList();
这个想法是分组两次。每本书一次,每个读者组一次。
首先,我们按ValueTuple (f1.BookTitle,f1.BookPages)
分组。创建Book
对象的好处是ValueTuple
自动覆盖Equals
和GetHashCode
。对于用作字典或查找中的键的类型(如GroupBy
),这是必需的。另外,您可以在Book
类中覆盖这些方法,并按Book
对象进行分组。如果您有唯一的图书ID,请改用此ID。
然后,我们使用Select
创建一个临时结果。我们再次创建一个具有3个字段的元组。包含书籍信息的元组,可枚举的Reader
对象,最后,我们创建一个包含所有读者作为关键字的字符串,稍后将使用该字符串按独特的读者组进行分组。如果您有唯一的读者ID,请使用此ID代替姓名和年龄。
到目前为止,我们有一个
IEnumerable<(
(string BookTitle,int BookPages) bookInfo,IEnumerable<Reader> readers,string readerKey
)>
现在,我们按readerKey
分组,然后创建ResultDoubleList
个对象的列表。
如果您在理解细节方面有困难,请将LINQ查询分为几个查询。通过使用“显式”重构,您可以查看得到的结果类型。 (这就是我从上方得到复杂的IEnumerable<T>
的方式。)这也使您可以在调试器中检查中间结果。
此测试...
int resultNo = 1;
foreach (ResultDoubleList item in result) {
Console.WriteLine($"\r\nresult({resultNo++}):");
Console.WriteLine("Books");
foreach (var book in item.Books) {
Console.WriteLine($" {book.Title,-28} {book.Pages,3}");
}
Console.WriteLine("Readers");
foreach (var reader in item.Readers) {
Console.WriteLine($" {reader.Name,-8} {reader.Age,2}");
}
}
Console.ReadKey();
...产量:
result(1):
Books
How to Do This Double List 105
Readers
Kyle 29
Bob 34
result(2):
Books
Gone With Jon Skeet 192
Why Is This So Hard? 56
Readers
Kyle 29
James 45
Brian 15
result(3):
Books
Impostor Syndrome 454
Self Doubt and You 999
Readers
Kyle 29
,
A。带有针对读者组的字符串键
var booksReadByGroups = flatManyToMany.GroupBy(a => a.BookTitle)
.Select(g => new
{
Book = new Book { Title = g.Key,Pages = g.Max(a => a.BookPages) },Readers = g.Select(a => new Reader { Name = a.ReaderName,Age = a.ReaderAge }).ToList()
})
.GroupBy(b => string.Join("+",b.Readers.OrderBy(r=>r.Name).ThenBy(r=>r.Age).Select(r => $"{r.Name}{r.Age}")))
.Select(g => new
{
Books = g.Select(b => b.Book),Readers = g.First().Readers
})
.ToList();
Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(booksReadByGroups));
以上内容产生了(有些行手动中断):
[{
"Books":[
{"Title":"How to Do This Double List","Pages":105}
],"Readers":[
{"Name":"Kyle","Age":29},{"Name":"Bob","Age":34}
]
},{
"Books":[
{"Title":"Gone With Jon Skeet","Pages":192},{"Title":"Why Is This So Hard?","Pages":56}
],{"Name":"James","Age":45},{"Name":"Brian","Age":15}
]
},{
"Books":[
{"Title":"Impostor Syndrome","Pages":454},{"Title":"Self Doubt and You","Pages":999}
],"Age":29}
]
}]
B。较短但较丑
我们需要两次GroupBy
,但是没有必要进行第一个投影,一个Select
就足够了。
var readerGroups = flatManyToMany.GroupBy(a => a.BookTitle)
.GroupBy(g => string.Join("+",g.OrderBy(r=>r.ReaderName).ThenBy(r=>r.ReaderAge).Select(r => $"{r.ReaderName}{r.ReaderAge}")))
.Select(g => new
{
Books = g.Select( g2 => new Book { Title = g2.Key,Pages = g2.Max(a => a.BookPages) }),Readers = g.First().Select(a => new Reader { Name = a.ReaderName,Age = a.ReaderAge })
});
C。使用IEquatable
此版本最长,但可以说是最正确的,因为它由Reader
类决定哪些读者被认为是平等的。
class ReadersComparer : IEqualityComparer<List<Reader>>
{
public bool Equals(List<Reader> a,List<Reader> b) => Enumerable.SequenceEqual(a,b); // Please note this doesn't order the lists so you either need to order them before,or order them here and implement IComparable on the Reader class
public int GetHashCode(List<Reader> os)
{
int hash = 19;
foreach (var o in os) { hash = hash * 31 + o.GetHashCode(); }
return hash;
}
}
public class Reader : IEquatable<Reader>
{
public string Name { get; set; }
public int Age { get; set; }
public override int GetHashCode() => (Name,Age).GetHashCode();
public bool Equals(Reader other) => (other is null) ? false : this.Name == other.Name && this.Age == other.Age;
public override bool Equals(object obj) => Equals(obj as Reader);
}
static void Main(string[] args)
{
var actsOfReading = new[]{
new Reading { BookTitle = "How to Do This Double List",BookPages = 105,ReaderName = "Kyle",ReaderAge = 29},new Reading { BookTitle = "How to Do This Double List",ReaderName = "Bob",ReaderAge = 34},new Reading { BookTitle = "Gone With Jon Skeet",BookPages = 192,ReaderName = "James",ReaderAge = 45},ReaderName = "Brian",ReaderAge = 15},new Reading { BookTitle = "Why Is This So Hard?",BookPages = 56,new Reading { BookTitle = "Impostor Syndrome",BookPages = 454,new Reading { BookTitle = "Self Doubt and You",BookPages = 999,ReaderAge = 29}
};
var booksReadByGroups = actsOfReading.GroupBy(a => a.BookTitle)
.Select(g => new
{
Book = new Book { Title = g.Key,Age = a.ReaderAge }).ToList()
})
.GroupBy(b => b.Readers,new ReadersComparer())
.Select(g => new
{
Books = g.Select(b => b.Book),Readers = g.First().Readers
})
.ToList();
Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(booksReadByGroups));
}
输出(手动格式)
[{
"Books": [{
"Title": "How to Do This Double List","Pages": 105 }
],"Readers": [{
"Name": "Kyle","Age": 29 },{
"Name": "Bob","Age": 34 }
]
},{
"Books": [{
"Title": "Gone With Jon Skeet","Pages": 192 },{
"Title": "Why Is This So Hard?","Pages": 56 }
],{
"Name": "James","Age": 45 },{
"Name": "Brian","Age": 15 }
]
},{
"Books": [{
"Title": "Impostor Syndrome","Pages": 454 },{
"Title": "Self Doubt and You","Pages": 999 }
],"Age": 29 }
]
}
]
,
假设书名和读者名是ID。
var results = flatManyToMany
.GroupBy(f => new { f.BookTitle,f.BookPages })
.Select(g => new
{
Book = new Book() { Title = g.Key.BookTitle,Pages = g.Key.BookPages },Readers = g.Select(i => new Reader() { Name = i.ReaderName,Age = i.ReaderAge })
})
.GroupBy(i => string.Concat(i.Readers.Select(r => r.Name).Distinct()))
.Select(g => new ResultDoubleList()
{
Books = g.Select(i => i.Book).ToList(),Readers = g.SelectMany(i => i.Readers).GroupBy(r => r.Name).Select(r => r.First()).ToList()
})
;
foreach(var result in results)
{
Console.WriteLine("Result:");
Console.WriteLine("\tBooks:");
foreach(var b in result.Books)
{
Console.WriteLine($"\t\t{b.Title}");
}
Console.WriteLine("\tReaders:");
foreach (var reader in result.Readers)
{
Console.WriteLine($"\t\t{reader.Name}");
}
}