كيفية تجميع البيانات حسب حقلين أو أكثر باستخدام LINQ في C#؟
\n\nمرحباً يا صديقي! إذا سبق لك كتابة استعلامات لقواعد البيانات بلغة SQL، فبالتأكيد تتذكر عبارة GROUP BY مع عدة أعمدة. فهي تسمح لك بتقسيم البيانات إلى سلال منطقية - على سبيل المثال، تجميع المبيعات أولاً حسب المدينة، ثم داخلياً حسب فئة المنتج. في لغة C#، يتم استخدام GroupBy من LINQ لنفس الغرض. ولكن المشكلة: GroupBy القياسي يقبل مفتاحاً واحداً فقط. ماذا لو كنت بحاجة إلى التجميع حسب عدة أعمدة؟
لا تقلق! الحل أنيق وبسيط - نقوم بإنشاء نوع مجهول يدمج الحقول المطلوبة في مفتاح مركب واحد. في هذه المقالة، سأعرض لك ليس فقط الصيغة الأساسية، ولكن أيضاً أمثلة حقيقية مع كود يمكنك تطبيقه فوراً في مشاريعك. هيا بنا!
\n\nالمشكلة: مفتاح واحد - هذا قليل جداً
\n\nتخيل أن لديك قائمة بالطلاب:
\n\npublic class Student\n{\n public string Name { get; set; }\n public string Course { get; set; }\n public int Grade { get; set; }\n}\n\nList<Student> students = new List<Student>\n{\n new Student { Name = "آنا", Course = "رياضيات", Grade = 85 },\n new Student { Name = "بوريس", Course = "رياضيات", Grade = 90 },\n new Student { Name = "آنا", Course = "فيزياء", Grade = 78 },\n new Student { Name = "بوريس", Course = "فيزياء", Grade = 82 },\n new Student { Name = "آنا", Course = "رياضيات", Grade = 95 }\n};\n\nتريد معرفة متوسط درجة كل طالب في كل مقرر. إذا استخدمت GroupBy حسب الاسم فقط، فسيتم خلط بيانات آنا في الرياضيات والفيزياء. إذا استخدمته حسب المقرر فقط، فسيتم فقدان الارتباط بالطالب. أنت بحاجة إلى تجميع حسب عمودين: الاسم + المقرر.
الحل: النوع المجهول كمفتاح مركب
\n\nفي لغة C#، يمكننا إنشاء كائن مؤقت بالحقول المطلوبة مباشرة في المكان:
\n\nvar grouped = students\n .GroupBy(s => new { s.Name, s.Course })\n .Select(g => new\n {\n g.Key.Name,\n g.Key.Course,\n AverageGrade = g.Average(st => st.Grade)\n });\n\nforeach (var item in grouped)\n{\n Console.WriteLine($"{item.Name} | {item.Course} | متوسط الدرجة: {item.AverageGrade:F2}");\n}\n\nالنتيجة:
\nآنا | رياضيات | متوسط الدرجة: 90.00
\nبوريس | رياضيات | متوسط الدرجة: 90.00
\nآنا | فيزياء | متوسط الدرجة: 78.00
\nبوريس | فيزياء | متوسط الدرجة: 82.00
لاحظ: new { s.Name, s.Course } هو النوع المجهول. يقوم LINQ تلقائياً بمقارنة الكائنات بناءً على جميع خصائصها (مساواة القيمة).
العمل مع IGrouping والمجموعات المتداخلة
\n\nأحياناً تحتاج ليس فقط إلى تجميع البيانات، بل إلى الاحتفاظ بجميع العناصر الأصلية داخل المجموعة. لهذا، يتم استخدام الواجهة IGrouping<TKey, TElement>. كل مجموعة تحتوي على مفتاح ومجموعة من العناصر:
var groups = students.GroupBy(s => new { s.Name, s.Course });\n\nforeach (var group in groups)\n{\n Console.WriteLine($"الطالب: {group.Key.Name}, المقرر: {group.Key.Course}");\n foreach (var student in group)\n {\n Console.WriteLine($" الدرجة: {student.Grade}");\n }\n}\n\nهذا الأسلوب مثالي عندما تحتاج، على سبيل المثال، إلى إنشاء تقرير مفصل.
\n\nصيغة الاستعلام (query) مقابل الصيغة التسلسلية (fluent) - أيهما تختار؟
\n\nيدعم LINQ أسلوبين لكتابة الاستعلامات. دعنا نرى كليهما:
\n\n1. الصيغة التسلسلية (Fluent)
\nvar result = students\n .GroupBy(s => new { s.Name, s.Course })\n .Select(g => new { g.Key.Name, g.Key.Course, Count = g.Count() });\n\n2. صيغة الاستعلام (Query)
\nvar result = from student in students\n group student by new { student.Name, stud