قبل ما نبدأ نتكلم عن الـ LINQ وهي إيه أصلًا، هناخد مقدمة بسيطة عن مشكلة قابلتنا وهنمشي خطوة بخطوة لحد ما نحلها بأحسن طريقة.
وده هيوضح لنا إيه فايدة الـ LINQ والناس اللي عملوها كانوا بيفكروا إزاي.
قبل ما نبدأ
نبدأ بمثال بسيط نفهم منه فايدتها وإزاي عملناها وليه؟
هنعمل Class اسمه Employee ونعمل منه List.
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public string Gender { get; set; }
public int Salary { get; set; }
public string Address { get; set; }
}
// In Main method or wherever you initialize
List<Employee> employees = new List<Employee>
{
new Employee { Id = 1, Name = "Ahmed", Gender = "Male", Salary = 6000, Address = "Cairo" },
new Employee { Id = 2, Name = "Salma", Gender = "Female", Salary = 4500, Address = "Alex" },
new Employee { Id = 3, Name = "Ali", Gender = "Male", Salary = 8000, Address = "Cairo" },
new Employee { Id = 4, Name = "Sara", Gender = "Female", Salary = 4800, Address = "Tanta" },
// Add more employees as needed
};Printing List of object
ممكن نستخدم الـ Foreach عشان نطبع الليستة دي.
foreach (var employee in employees)
{
Console.WriteLine(employee);
}
// Output will likely be something like:
// YourNameSpace.Employee
// YourNameSpace.Employee
// ...هنلاقي إنه بيطبع اسم الـ namespace واسم الـ class بالشكل ده. ده بسبب الـ ToString() method اللي موجودة تلقائي في الـ Cs System.Object، اللي أي class في C# بيورث منه بشكل تلقائي.
عشان نخلي الطباعة شكلها أحسن، ممكن نعمل override للـ ToString() method دي جوه الـ Employee class.
// Inside Employee Class
public override string ToString()
{
// Customize the string representation
return $"Id: {Id} - Name: {Name} - Gender: {Gender} - Salary: {Salary} - Address: {Address}";
}دلوقتي لو طبعنا تاني، هيظهر بيانات كل موظف بشكل أوضح.
مقدمة: نعمل Query للداتا
نفترض إننا عايزين نجيب كل الموظفين اللي المرتب بتاعهم أقل من 5000 جنيه.
الموظفين اللي مرتبهم أقل من 5000
الطريقة القديمة (Old way)
كنا بنعمل loop ونحط if condition ونضيف الموظفين اللي بيحققوا الشرط لـ list جديدة.
List<Employee> filteredList = new List<Employee>();
foreach (var employee in employees)
{
if (employee.Salary < 5000)
{
filteredList.Add(employee);
}
}
// Print the filtered list
Console.WriteLine("Employees with Salary < 5000:");
foreach (var employee in filteredList)
{
Console.WriteLine(employee); // Uses the overridden ToString()
}مشكلة الطريقة دي:
- لو استخدمنا نفس الـ
logicده (المرتب أقل من 5000) في أكتر من مكان في الكود، وجينا بعد كده حبينا نغير الشرط (مثلًا نخليه أقل من 6000)، هنضطر نعدّل في كل الأماكن دي. ده بيزود احتمالية الخطأ وبيصعب الصيانة.
فالحل إننا نعمل Method بتعمل الفلترة دي، عشان لو حبينا نغير الـ logic، نغيره في مكان واحد بس. والأحسن كمان إننا نعملها كـ Extension Method عشان تبقى سهلة الاستخدام مع أي List<Employee>.
نعمل ميثود (Make Method)
هنعمل static class ونضيف فيه extension method للـ List<Employee>.
public static class ListExtensions
{
// Specific filter method (not ideal)
public static List<Employee> FilterEmployeesWithSalaryLessThan5000(this List<Employee> employees)
{
List<Employee> filteredEmployees = new List<Employee>();
foreach (var employee in employees)
{
if (employee.Salary < 5000)
{
filteredEmployees.Add(employee);
}
}
return filteredEmployees;
}
}
// How to use it:
// var filteredByMethod = employees.FilterEmployeesWithSalaryLessThan5000();كده حلينا مشكلة تكرار الكود وتعديله في كذا مكان.
طيب، لو عايز أعمل فلترة تانية؟ مثلًا، أجيب الموظفين الذكور بس (Gender == "Male") أو اللي ساكنين في طنطا (Address == "Tanta")؟ هل هعمل method جديدة لكل شرط؟ أكيد لأ.
استخدام الـ Cs Delegates
لو بصينا على الـ methods اللي ممكن نعملها للفلترة المختلفة:
// Filter by Tanta Address (Old way - specific method)
public static List<Employee> FilterEmployeesFromTanta(this List<Employee> employees)
{
List<Employee> filteredEmployees = new List<Employee>();
foreach (var employee in employees)
{
if (employee.Address == "Tanta") // The only difference is this condition
{
filteredEmployees.Add(employee);
}
}
return filteredEmployees;
}
// Filter Male Employees (Old way - specific method)
public static List<Employee> FilterMaleEmployees(this List<Employee> employees)
{
List<Employee> filteredEmployees = new List<Employee>();
foreach (var employee in employees)
{
if (employee.Gender == "Male") // The only difference is this condition
{
filteredEmployees.Add(employee);
}
}
return filteredEmployees;
}هنلاحظ إن الفرق الوحيد بين الـ methods دي هو الشرط (if condition).
الحل هنا إننا نخلي الـ method بتاعة الفلترة تاخد الشرط ده كـ parameter. وعشان الشرط ده عبارة عن function صغيرة بترجع true أو false، هنستخدم delegate مناسب. الـ delegate الأنسب هنا هو Predicate لإنها بتاخد parameter من نوع T (اللي هو Employee في حالتنا) وبترجع bool.
// In the ListExtensions class
public static class ListExtensions
{
// Generic filter method using Predicate delegate
public static List<Employee> FilterEmployees(this List<Employee> employees, Predicate<Employee> predicate)
{
List<Employee> filteredEmployees = new List<Employee>();
foreach (var employee in employees)
{
// Call the predicate to check the condition
if (predicate(employee))
{
filteredEmployees.Add(employee);
}
}
return filteredEmployees;
}
}
// In the Main method or wherever you call it:
// Define boolean methods matching the Predicate signature
public static bool IsMaleEmployee(Employee employee)
{
return employee.Gender == "Male";
}
public static bool IsSalaryLessThan5000(Employee employee) // Changed from 8000 in original text
{
return employee.Salary < 5000;
}
public static bool IsFromTanta(Employee employee)
{
return employee.Address == "Tanta";
}
// Call the generic filter method, passing the condition method as a parameter
var maleEmployees = employees.FilterEmployees(IsMaleEmployee);
var lowSalaryEmployees = employees.FilterEmployees(IsSalaryLessThan5000);
var tantaEmployees = employees.FilterEmployees(IsFromTanta);
// Print results (example for males)
Console.WriteLine("\nMale Employees:");
foreach (var employee in maleEmployees)
{
Console.WriteLine(employee);
}وبكده المشكلة بقت محلولة بشكل كويس جدًا. عندنا method واحدة للفلترة، وبنمرر لها الشرط اللي عايزينه.
طيب، لو احتجنا نعمل class جديد، مثلًا Product، ونعمل له فلترة بنفس الطريقة؟ هل هنكرر نفس الـ FilterEmployees method ونعمل FilterProducts؟ أكيد لأ.
هنا ييجي دور الـ Generics.
استخدام الـ Cs Generics
عشان نخلي الـ Filter method بتاعتنا تشتغل مع أي type (سواء Employee أو Product أو غيره)، هنستخدم الـ Generics. هنخلي الـ method تاخد type parameter (T) وتشتغل عليه.
// Updated ListExtensions class with Generics
public static class GenericListExtensions // Renamed for clarity
{
// Generic Filter method using Generics and Predicate
public static List<T> Filter<T>(this List<T> items, Predicate<T> predicate)
{
List<T> filteredItems = new List<T>();
foreach (var item in items)
{
if (predicate(item))
{
filteredItems.Add(item);
}
}
return filteredItems;
}
}
// Example Product class
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string Category { get; set; }
public double Price { get; set; }
public override string ToString() => $"Id: {Id}, Name: {Name}, Category: {Category}, Price: {Price}";
}
// In Main method:
List<Product> products = new List<Product>
{
new Product { Id = 101, Name = "Laptop", Category = "Devices", Price = 25000 },
new Product { Id = 102, Name = "Keyboard", Category = "Devices", Price = 1500 },
new Product { Id = 103, Name = "T-Shirt", Category = "Clothing", Price = 500 },
new Product { Id = 104, Name = "Mouse", Category = "Devices", Price = 800 },
};
// Define predicate methods for Product
public static bool IsDeviceAndLessThan20000(Product product)
{
return product.Category == "Devices" && product.Price < 20000;
}
// Call the generic Filter method
var filteredEmployeesGeneric = employees.Filter<Employee>(IsMaleEmployee); // Specify Employee type
var filteredProductsGeneric = products.Filter<Product>(IsDeviceAndLessThan20000); // Specify Product type
// Print results (example for products)
Console.WriteLine("\nFiltered Products (Devices < 20000):");
foreach (var product in filteredProductsGeneric)
{
Console.WriteLine(product);
}دلوقتي الـ Filter<T> method بقت شغالة مع أي List وأي شرط.
عشان نخلي الـ Syntax أسهل وأكتر اختصارًا، بدل ما نعمل method منفصلة لكل شرط (زي IsMaleEmployee و IsDeviceAndLessThan20000)، ممكن نستخدم Cs Lambda Expressions. غالبًا مش هنحتاج نستخدم الـ methods دي تاني، فالـ Lambda بتكون أنسب.
// Using Lambda Expressions directly with the Filter method
var filteredEmployeesLambda = employees.Filter(employee => employee.Gender == "Male");
var filteredProductsLambda = products.Filter(product => product.Category == "Devices" && product.Price < 20000);
// Note: We don't need to specify <Employee> or <Product> because the compiler infers it.
// Print results (example for products lambda)
Console.WriteLine("\nFiltered Products (Lambda):");
foreach (var product in filteredProductsLambda)
{
Console.WriteLine(product);
}Where (LINQ)
كل اللي عملناه فوق ده عشان نوصل لـ method فلترة عامة ومرنة، هو بالظبط اللي LINQ بيقدمهولنا جاهز في operator اسمه Where.
الـ Where هو Filtration Operator في LINQ.
// The Filter<T> method we created:
// var filteredProductsManual = products.Filter(product => product.Category == "Devices" && product.Price < 20000);
// Using LINQ's built-in Where (Fluent Syntax):
var filteredProductsLinqFluent = products.Where(product => product.Category == "Devices" && product.Price < 20000);
// Using LINQ's built-in Where (Query Syntax):
var filteredProductsLinqQuery = from p in products // p is a range variable representing each product
where p.Category == "Devices" && p.Price < 20000
select p; // Select the product itself
// Print results (example for LINQ Fluent)
Console.WriteLine("\nFiltered Products (LINQ Fluent Where):");
foreach (var product in filteredProductsLinqFluent)
{
Console.WriteLine(product);
}زي ما إنت شايف، LINQ بيوفر لنا Where اللي بتعمل نفس وظيفة الـ Filter<T> بتاعتنا، بس بشكل مدمج في اللغة.
Indexed Where
الـ Where method ليها overload.
- الأول (اللي استخدمناه): بياخد
parameterواحد بس (اللي هو الـpredicateاللي بيشتغل على العنصر نفسه، زيproduct => ...). - التاني (Indexed Where): بياخد اتنين
parameters:- العنصر نفسه (
product). - الـ
indexبتاع العنصر ده في الـsequence(بيبدأ من 0).
- العنصر نفسه (
فمثلًا، لو عايز أجيب أول 10 منتجات بشرط يكونوا من نوع Devices:
// Using Indexed Where (Fluent Syntax only)
var first10Devices = products.Where((p, index) => index < 10 && p.Category == "Devices");
Console.WriteLine("\nFirst 10 Devices (Indexed Where):");
foreach (var product in first10Devices)
{
Console.WriteLine(product);
}ملحوظة مهمة: الـ Indexed Where ده متاح بس في الـ Fluent Syntax (طريقة الـ methods)، ومش متاح في الـ Query Syntax (طريقة from...where...select). هنوضح الفرق بينهم تحت.
LINQ
مقدمة
- LINQ stands for Language Integrated Query. (استعلام مدمج في اللغة).
- بنقدر نعمل بيها
Queries(استعلامات) على الداتا بتاعتنا بسهولة جوه الكود بتاع C#. - هي عبارة عن مجموعة كبيرة من الـ
functions(أكتر من 40، وكل واحدة ليها أكتر منoverload). - الـ
functionsدي في الحقيقة عبارة عن Extension Methods معمولة للـinterfaceاللي اسمه IEnumerable وأيclassبيعمل لهimplementأو بيورث منه (زيList,Array, IReadOnlyList وغيرهم كتير). - الـ
functionsدي بنسميها برضو LINQ Operators وكلهم موجودين جوهstatic classاسمه Enumerable. - الـ
Operatorsدي متقسمة لـ 13 Category (مجموعة) حسب وظيفتها (زي Filtration, Projection, Ordering, Grouping, etc.). - طبعًا، الـ
Return typeبتاع معظم الـLINQ operatorsبيكونIEnumerable(أوIQueryableزي ما هنشوف)، وممكن نحول النتيجة دي لـListأوArrayبسهولة لو محتاجين (باستخدامToList()أوToArray()).
طب إيه علاقتها بالـ Databases وفايدتها؟
- الـ
LINQ functionsأوOperatorsدي تعتبر تطبيق للـconceptsبتاعة لغة استعلام البيانات القياسيةSQL، وبالتحديد الجزء الخاص بالـ Data Query Language (DQL) اللي هو أمرSELECTبكل الإمكانيات اللي معاه (زيWHERE,ORDER BY,GROUP BY,JOIN). يعني كأننا خدنا الـSELECTstatement وحطيناها جوه C#. - الفائدة الكبيرة: بدل ما نكتب
SQL queryصريح في الكود بتاعنا (وده هيخلينا مرتبطين بـdatabase serverمعين زي SQL Server أو MySQL)، بنكتبLINQ query. - الـ
LINQ queryده بيكون مستقل عن نوع الـdatabase. - لما بنستخدم
LINQمعframeworkزي Entity Framework Core، الـEF Coreهو اللي بيتولى مهمة ترجمة الـLINQ queryبتاعنا لـSQL queryمناسب للـDatabase Management System (DBMS)اللي إحنا شغالين عليه (سواء كان SQL Server, PostgreSQL, SQLite, etc.). - ده بيخلي الكود بتاعنا
portableأكتر، لو حبينا نغير نوع الـdatabaseفي المستقبل، مش هنحتاج نغير الـLINQ queriesبتاعتنا.
Sequence (المتسلسلة)
إحنا بنستخدم الـ LINQ Operators مع الداتا اللي بتكون متخزنة في شكل Sequence (متسلسلة)، بغض النظر هي متخزنة فين.
لما نقول “Sequence”، بنقصد أي Object بيطبق الـ interface اللي اسمه Cs IEnumerable (وده معناه إننا نقدر نعمل عليه loop باستخدام foreach). أمثلة زي: List<T>, T[] (Array), Dictionary<TKey, TValue>, string, وغيرها من الـ Collections.
التقسيم لـ “Local” و “Remote” بيعتمد على مصدر الداتا:
-
Local Sequence:
- هي أي
Sequenceموجودة في الذاكرة (in-memory) بتاعة الأبلكيشن بتاعك، زيListأوArrayعملتهم بنفسك. - لما بتستخدم
LINQمعاها، العملية دي بنسميها LINQ to Objects (L2O). - فيه كمان LINQ to XML (L2XML) لو بتعمل
queryعلى فايل XML files موجود عندك. - كل العمليات (الفلترة، الترتيب، إلخ) بتحصل جوه الـ
RAMبتاعة الجهاز اللي شغال عليه الأبلكيشن. مفيش اتصال بمصدر خارجي. - حتى فايلات الـ
XMLبنعتبرهاLocalفي السياق ده.
- هي أي
-
Remote Sequence:
- هي
Sequenceالداتا بتاعتها مش موجودة مباشرة في الذاكرة، لكنها جاية من مصدر بيانات خارجي (external data source). - أشهر مثال هو قاعدة البيانات (Database). أمثلة تانية ممكن تكون
Web APIأو أي خدمة بتوفر داتا. - لما بنشتغل مع
remote sequences(خصوصًا معdatabasesباستخدامEF Core)، غالبًا بنتعامل معinterfaceتاني اسمه Cs IQueryable بدلIEnumerable<T>. - الـ
IQueryable<T>بيسمح بترجمة الـLINQ queryلـquery languageيفهمها المصدر الخارجي (زيSQLفي حالة الـdatabase). - العمليات دي بنسميها LINQ to SQL (للـ
SQL Serverبشكل مباشر زمان)، LINQ to Entities (معEntity FrameworkوEF Core)، أو LINQ to OData (لو بتكلمOData API). - الـ
Queryنفسه بيتنفذ على الـserverأو المصدر الخارجي، وبعدين النتيجة بس هي اللي بترجع للأبلكيشن بتاعك.
- هي
باختصار:
- الـ Local Sequence: موجودة جوه الأبلكيشن وتقدر تلف عليها بـ
foreachفي الـmemoryعلطول. - الـ Remote Sequence: بتمثل داتا موجودة بره الأبلكيشن (زي
database table)، الـqueryبيتنفذ هناك والنتيجة بترجعلك.
Query Syntax vs Fluent Syntax
فيه طريقتين أساسيتين لكتابة LINQ queries في C#:
النوع اللي هو الـ Method دا اسمه Fluent برضو.
1. Fluent Syntax (Method Syntax)
دي الطريقة اللي بنستخدم فيها الـ LINQ operators كأنها methods بنناديها على الـ sequence بتاعتنا. ليها شكلين:
- الـ Extension Method (الطريقة الشائعة والمفضلة): بننادي الـ
operatorكـextension methodمباشرة على الـListأو الـIEnumerableبتاعنا. - الـ Static Method: بننادي الـ
operatorكـstatic methodمن الـEnumerableclass وبنمرر له الـsequenceكأولparameter.
List<int> Numbers = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
IEnumerable<int> OddNumbers; // Result will be IEnumerable
// الطريقة الأولى (Static Method - مش بنستخدمها كتير أوي)
// Enumerable.Where(Source_Sequence, Predicate_Lambda)
OddNumbers = Enumerable.Where(Numbers, N => N % 2 == 1);
Console.WriteLine("Odd Numbers (Static Method): " + string.Join(", ", OddNumbers));
// الطريقة التانية (Extension Method - دي اللي بنستخدمها دايمًا)
OddNumbers = Numbers.Where(N => N % 2 == 1);
Console.WriteLine("Odd Numbers (Extension Method): " + string.Join(", ", OddNumbers));الـ Fluent Syntax (خصوصًا طريقة الـ Extension Method) هي الأكثر استخدامًا ومرونة، خصوصًا مع العمليات المعقدة أو المتسلسلة (chaining).
2. Query Syntax (Query Expression Syntax)
- دي طريقة كتابة
LINQبشكل شبه لغةSQL. - بتبدأ دايمًا بكلمة
fromاللي بتعرفrange variableبيمثل كل عنصر في الـsequence(زيNفي المثال تحت). - بعدها بتيجي جمل زي
where,orderby,select,group,join. - لازم تنتهي بـ
selectأوgroup by. - الترتيب بتاع الجمل في الـ
Query Syntaxبيكون مختلف شوية عنSQL(مثلًاselectبتيجي في الآخر).
// Query Syntax example for odd numbers
OddNumbers = from N in Numbers // Define range variable N for each item in Numbers
where N % 2 == 1 // Filter condition
select N; // Select the item itself
Console.WriteLine("Odd Numbers (Query Syntax): " + string.Join(", ", OddNumbers));الـ Query Syntax ممكن تكون أسهل في القراءة لبعض الناس اللي متعودين على SQL، خصوصًا مع عمليات الـ Join والـ Grouping.
3. Hybrid Syntax (الخليط)
- أحيانًا بنحتاج نستخدم
operatorمش موجود أو صعب استخدامه في الـQuery Syntax(زيCount(),FirstOrDefault(),Take(),Skip(), وغيرها كتير). - في الحالة دي، ممكن نكتب جزء من الـ
queryبالـQuery Syntax، ونحط النتيجة بين قوسين()، وبعدين نكمل عليه بالـFluent Syntax.
// Example: Get the first product that is out of stock using Hybrid Syntax
// Assuming ProductList is a List<Product>
// var Result = (from P in ProductList // Query Syntax part
// where P.UnitsInStock == 0
// select P)
// .FirstOrDefault(); // Fluent Syntax part added after the query
// Example with the sample data (find first product with price < 1000)
var firstCheapProduct = (from p in products
where p.Price < 1000
select p)
.FirstOrDefault(); // Returns the Mouse product or null if none found
if (firstCheapProduct != null)
{
Console.WriteLine($"\nFirst Cheap Product (Hybrid): {firstCheapProduct}");
}
else
{
Console.WriteLine("\nNo cheap product found (Hybrid).");
}أنهي طريقة أستخدمها؟
- الأمر بيرجع للتفضيل الشخصي والموقف.
- الـ Fluent Syntax: أكثر قوة وشمولية (كل الـ
operatorsمتاحة)، وممتاز لـchaining methods. - الـ Query Syntax: أسهل في القراءة لعمليات الـ
JoinوالـGroupingالمعقدة شوية. - الـ Hybrid Syntax: بنستخدمه لما نحتاج ندمج بين الطريقتين.
- كتير من المبرمجين بيستخدموا
Fluent Syntaxفي معظم الوقت، وبيلجأوا لـQuery Syntaxفي حالات الـJoin/Groupالصعبة.
تجهيز الداتا (Data Setup)
عشان نبدأ نشتغل فعليًا ونجرب الـ LINQ operators المختلفة، هنحتاج نعمل dummy project أو نستخدم classes و lists فيها داتا تجريبية نقدر نعمل عليها queries. هنستخدم الـ Employee و Product lists اللي عملناهم فوق كأمثلة.
هتلاقيه هنا Data Setup for LINQ
(هنفترض إن الداتا دي موجودة في باقي الأمثلة)
Deferred Execution VS Immediate Execution
دي نقطة مهمة جدًا في فهم LINQ بيشتغل إزاي.
1. Deferred Execution (التنفيذ المؤجل)
- معظم
LINQ operators(زيWhere,Select,OrderBy,GroupBy,Join, وغيرها كتير) مش بتتنفذ أول ما تكتبها. - لما بتكتب
queryباستخدام الـoperatorsدي، اللي بيحصل إنLINQبيسجل الخطوات أو الـexpressionبتاع الـqueryده، وبيديلك كأنها “وعد” (Promise) أو وصفة للتنفيذ. - الـ
Queryمش بيتنفذ فعليًا إلا لما تيجي تطلب النتيجة بتاعته. إمتى بنطلب النتيجة؟- لما نعمل
loopعلى الـresultباستخدامforeach. - لما ننادي على
methodمن اللي بتعملImmediate Execution(زيToList(),ToArray(),Count(),First(),Sum(), etc.).
- لما نعمل
- على مستوى الـ
Memory، الـvariableاللي بيشيل نتيجة الـdeferred query(زيevenNumbersفي المثال تحت) بيخزن بس الوصفة بتاعة الـqueryوبيحتفظ بـreferenceللـoriginal list(زيnumbers). - لما تيجي تطلب النتيجة (مثلًا بـ
foreach)، ساعتها بس الـqueryبيروح يتنفذ على الـoriginal listفي الوقت ده بحالتها الحالية.
مثال يوضح الـ Deferred Execution:
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Define the query using Where (Deferred Execution operator)
var evenNumbersQuery = numbers.Where(n => n % 2 == 0);
// <<< Query is NOT executed yet! 'evenNumbersQuery' only holds the definition. >>>
Console.WriteLine("Modifying the list AFTER defining the query...");
numbers.Add(12); // Add a new even number
numbers.Add(14); // Add another new even number
numbers.Remove(4); // Remove an existing even number
Console.WriteLine("Iterating through the query result (Execution happens NOW):");
// The query executes here because we are asking for the results via foreach
foreach (var item in evenNumbersQuery)
{
Console.Write(item + " "); // Output will include 12, 14 and exclude 4
}
Console.WriteLine();
// Expected output: 2 6 8 10 12 14ليه الـ LINQ بتشتغل بالطريقة المؤجلة دي؟ إيه فوايدها؟
- الـ Performance (الأداء):
- الـ
Queryمش بيتنفذ إلا لما تحتاجه فعلًا. لو عرفتqueryومعملتش عليهloopأو محولتهوش لـList، مش هيستهلك أي وقت تنفيذ تقريبًا. - لو عندك
queryبيرجع داتا كتير، وانت محتاج بس أول عنصر (FirstOrDefault()) أو أول كام عنصر (Take(5)), الـqueryهيتنفذ بس لحد ما يجيب المطلوب ومش هيكمل باقي الداتا.
- الـ
- الـ Memory Efficiency (كفاءة الذاكرة):
- النتائج مش بتتخزن في الـ
memoryكلها مرة واحدة (إلا لو طلبت ده بـToList()مثلًا). ده كويس جدًا لو بتتعامل مع حجم داتا كبير جدًا ممكن ميكفيش في الـmemory.
- النتائج مش بتتخزن في الـ
- الـ Composability (التركيب):
- تقدر تبني
queryمعقد على مراحل. ممكن تعرفqueryأساسي، وبعدين تضيف عليهfilterتاني (.Where(...)) أو ترتيب (.OrderBy(...)) حسب الحاجة، وكل ده هيتجمع وهيتركب مع بعض ويتنفذ مرة واحدة في الآخر.
- تقدر تبني
- الـ Always Fresh Data (دايمًا أحدث داتا):
- زي ما شفنا في المثال، لإن الـ
queryبيتنفذ لما تطلب النتيجة، فهو دايمًا بيشتغل على آخر نسخة من الداتا اللي في الـsource list. لو عدلت في الـlistبعد ما عرفت الـqueryوقبل ما تنفذه، التعديلات دي هتنعكس في النتيجة.
- زي ما شفنا في المثال، لإن الـ
- الـ Working with potentially infinite sequences:
- نظريًا، ممكن تعمل
queryعلىsequenceمش بتنتهي (مثلًاgenerator functionبتطلع أرقام للأبد)، وتاخد منها جزء (Take(100)) من غير ما تحتاج تحمل الـsequenceكلها.
- نظريًا، ممكن تعمل
الـ Categories اللي بتشتغل Deferred Execution:
معظم الـ Categories (حوالي 10 من الـ 13) بتشتغل بالطريقة دي، زي:
- Filtration (
Where) - Projection (
Select,SelectMany) - Ordering (
OrderBy,ThenBy,Reverse) - Grouping (
GroupBy) - Joining (
Join,GroupJoin) - Set (
Union,Intersect,Except,Distinct,Concat) - Partitioning (
Take,Skip,TakeWhile,SkipWhile,Chunk) - Generation (
Range,Repeat,Empty) - Zipping (
Zip)
2. Immediate Execution (التنفيذ الفوري)
- على عكس الـ
Deferred، فيه بعض الـLINQ operatorsبتنفذ الـqueryفورًا أول ما تنادي عليها. - الـ
Operatorsدي غالبًا بتكون اللي:- بترجع قيمة واحدة (single value) من الـ
sequence(زيCount(),Sum(),Max(),First(),ElementAt()). - بتحول الـ
sequenceلـ collection تانية في الـmemory(زيToList(),ToArray(),ToDictionary(),ToHashSet()).
- بترجع قيمة واحدة (single value) من الـ
- لما بتستخدم واحد من الـ
operatorsدي، الـqueryاللي قبله (لو فيه) بيتنفذ كله علطول، والنتيجة بتتحسب وتتخزن أو تترجع.
مثال يوضح الـ Immediate Execution:
List<int> numbers2 = new List<int> { 1, 2, 3, 4, 5 };
// Define the query AND execute it immediately using ToList()
List<int> evenNumbersList = numbers2.Where(n => n % 2 == 0).ToList();
// <<< Query executed NOW! 'evenNumbersList' holds [2, 4] in memory. >>>
Console.WriteLine("\nModifying the list AFTER immediate execution...");
numbers2.Add(6); // Add a new even number
numbers2.Add(8); // Add another new even number
Console.WriteLine("Iterating through the immediate result list:");
// We are iterating through the list created earlier, not re-executing the query
foreach (var item in evenNumbersList)
{
Console.Write(item + " "); // Output will only be the numbers captured initially
}
Console.WriteLine();
// Expected output: 2 4الفرق بين الـ Deferred والـ Immediate باختصار:
- الـ Deferred:
- التنفيذ بيتأجل لحد ما تطلب النتيجة (
foreach,ToList(), etc.). - النتيجة دايمًا بتعكس آخر حالة للداتا.
- أمثلة:
Where(),Select(),OrderBy().
- التنفيذ بيتأجل لحد ما تطلب النتيجة (
- الـ Immediate:
- التنفيذ بيحصل فورًا أول ما تنادي الـ
operator. - النتيجة بتتحسب وتتخزن (لو
ToListمثلًا) أو تترجع (لوCountمثلًا). - أي تعديلات على الداتا الأصلية بعد التنفيذ مش بتأثر على النتيجة اللي اتحسبت خلاص.
- أمثلة:
ToList(),ToArray(),Count(),First(),Sum().
- التنفيذ بيحصل فورًا أول ما تنادي الـ
إمتى نستخدم الـ Immediate Execution؟
- لما تكون عايز تحفظ “لقطة” (snapshot) من نتيجة الـ
queryفي لحظة معينة، ومش عايزها تتأثر بأي تغييرات هتحصل بعد كده في الداتا الأصلية. - لما تكون هتستخدم نتيجة الـ
queryده كذا مرة في الكود بتاعك، فتحويلها لـListأوArrayمرة واحدة ممكن يكون أكفأ من إعادة تنفيذ الـqueryكل مرة. - لما تكون النتيجة النهائية للـ
queryهي قيمة واحدة (زي عدد العناصر أو مجموعهم).
الـ Categories اللي بتشتغل Immediate Execution:
فيه 3 Categories أساسية بتشتغل بالطريقة دي:
- الـ Element Operators اللي بتجيب عنصر واحد (زي
First,Last,Single,ElementAtومشتقاتهم). - الـAggregate Operator اللي بتعمل عمليات حسابية أو تجميعية على كل الـ
sequenceعشان ترجع قيمة واحدة (زيCount,Sum,Average,Min,Max,Aggregate,Any,All). - الـCasting Operators اللي بتحول الـ
sequenceلنوعcollectionتاني (زيToList,ToArray,ToDictionary,ToLookup,ToHashSet).
ملحوظة: لو عايز تجبر query معمول بـ deferred operators إنه يتنفذ فورًا، ممكن تضيف في آخره operator من بتوع الـ immediate execution (زي .ToList()).
LINQ (LINQ Operations / Operators)
زي ما قلنا، LINQ فيه أكتر من 40 operator متقسمين لـ 13 category. اتكلمنا فوق عن أول category اللي هو الـ Filtration Operators وكان أهم واحد فيه هو Where.
هناخد دلوقتي باقي الـ categories المشهورة والمهمة:
1. Projection or Transformation Operators (تحويل الشكل)
الـ Operators دي بتاخد كل عنصر في الـ sequence الأصلية وتحوله لشكل تاني أو تختار جزء منه. أشهرهم Select و SelectMany. (دول Deferred Execution).
Select()
- بتاخد كل عنصر في الـ
collectionوتحوله لحاجة تانية (ممكن تكونpropertyمعينة من العنصر، أوobjectجديد خالص). - بترجع
sequenceجديدة بنفس حجم الـsequenceالأصلية، بس العناصر اللي جواها شكلها متغير.
مثال: اختيار أسماء الموظفين بس:
// Select just the names of the employees
var employeeNames = employees.Select(employee => employee.Name);
// employeeNames is now an IEnumerable<string>
Console.WriteLine("\nEmployee Names (Select):");
foreach (var name in employeeNames)
{
Console.WriteLine(name);
}
// Query Syntax equivalent:
var employeeNamesQuery = from e in employees
select e.Name;مثال: اختيار أكتر من property (باستخدام Anonymous Type):
مينفعش أرجع أكتر من property مباشرة بـ Select. لو عايز أرجع مثلًا اسم وعمر الموظف، لازم أرجعهم في object جديد.
الحل السهل والمباشر هنا هو استخدام Anonymous Type:
// Select Name and Age into an Anonymous Type
var nameAndAge = employees.Select(e => new { EmployeeName = e.Name, EmployeeAge = e.Salary }); // Assuming Salary represents Age for example
// nameAndAge is now an IEnumerable of objects with Name and Age properties
Console.WriteLine("\nEmployee Name and Age (Select with Anonymous Type):");
foreach (var item in nameAndAge)
{
Console.WriteLine($"Name: {item.EmployeeName}, Age: {item.EmployeeAge}");
// You can access properties by the names you gave them (EmployeeName, EmployeeAge)
}
// Using aliases for shorter names in the anonymous type:
var nameAndAgeShorter = employees.Select(e => new { N = e.Name, A = e.Salary });
foreach (var item in nameAndAgeShorter)
{
Console.WriteLine($"N: {item.N}, A: {item.A}");
}
// Query Syntax equivalent for Anonymous Type:
var nameAndAgeQuery = from e in employees
select new // Create anonymous type
{
EmployeeName = e.Name,
EmployeeAge = e.Salary
};- الـ
Anonymous Typeدهtypeملوش اسم بنعرفه في الكود، الـCLRهو اللي بيعمله في الـruntime. - لو عايز تعمل
assignلـvariableفيهanonymous typeلـvariableتاني، لازم الـpropertiesتكون بنفس الأسماء والـtypesوالترتيب بالظبط.
Indexed Select:
زي الـ Indexed Where، فيه overload لـ Select بياخد الـ index كـ parameter تاني. متاح بس في الـ Fluent Syntax.
// Select Name along with its index in the original list
var namesWithIndex = employees.Select((emp, index) => new { Index = index, Name = emp.Name });
Console.WriteLine("\nNames with Index (Indexed Select):");
foreach (var item in namesWithIndex)
{
Console.WriteLine($"Index: {item.Index}, Name: {item.Name}");
}SelectMany()
- دي بنستخدمها لما يكون عندنا
sequence of sequences(يعني مثلًاListجواهاListتانية) وعايزين نعملها “فرد” أو “تسطيح” (Flattening) عشان تبقىsequenceواحدة بس. - تخيل عندك ليستة موظفين، وكل موظف عنده ليستة بالمهارات (
Skills) بتاعته. لو عايز تجيب كل المهارات الموجودة عند كل الموظفين في ليستة واحدة، هنا بنستخدمSelectMany.
مثال:
// Assume Employee class has a List<string> Skills property
public class EmployeeWithSkills
{
public int Id { get; set; }
public string Name { get; set; }
public List<string> Skills { get; set; }
public override string ToString() => $"Id: {Id}, Name: {Name}, Skills: {string.Join(", ", Skills)}";
}
List<EmployeeWithSkills> employeesWithSkills = new List<EmployeeWithSkills>
{
new EmployeeWithSkills { Id = 1, Name = "Ahmed", Skills = new List<string> { "C#", "SQL", "ASP.NET" } },
new EmployeeWithSkills { Id = 2, Name = "Mona", Skills = new List<string> { "HTML", "CSS", "JavaScript" } },
new EmployeeWithSkills { Id = 3, Name = "Ali", Skills = new List<string> { "C#", "Azure", "Docker" } },
new EmployeeWithSkills { Id = 4, Name = "Sara", Skills = new List<string> { "JavaScript", "React", "Node.js" } }
};
// Using SelectMany to get a single list of all skills
var allSkills = employeesWithSkills.SelectMany(emp => emp.Skills);
// allSkills is now IEnumerable<string> containing all skills from all employees
Console.WriteLine("\nAll Skills (SelectMany):");
Console.WriteLine(string.Join(", ", allSkills));
// Output: C#, SQL, ASP.NET, HTML, CSS, JavaScript, C#, Azure, Docker, JavaScript, React, Node.js
// If you want only unique skills, combine SelectMany with Distinct
var uniqueSkills = employeesWithSkills.SelectMany(emp => emp.Skills).Distinct();
Console.WriteLine("\nUnique Skills (SelectMany + Distinct):");
Console.WriteLine(string.Join(", ", uniqueSkills));
// Output: C#, SQL, ASP.NET, HTML, CSS, JavaScript, Azure, Docker, React, Node.js
// Query Syntax equivalent for SelectMany:
// Uses multiple 'from' clauses
var allSkillsQuery = from emp in employeesWithSkills // Outer loop for employees
from skill in emp.Skills // Inner loop for skills within each employee
select skill; // Select the skill
Console.WriteLine("\nAll Skills (Query Syntax):");
Console.WriteLine(string.Join(", ", allSkillsQuery));
var uniqueSkillsQuery = (from emp in employeesWithSkills
from skill in emp.Skills
select skill).Distinct(); // Add Distinct for unique
Console.WriteLine("\nUnique Skills (Query Syntax + Distinct):");
Console.WriteLine(string.Join(", ", uniqueSkillsQuery));
الفرق بين Select و SelectMany في الحالة دي:
لو كنت استخدمت Select بدل SelectMany في المثال بتاع المهارات:
var skillsUsingSelect = employeesWithSkills.Select(emp => emp.Skills);
// skillsUsingSelect is IEnumerable<List<string>> (a list of lists)
Console.WriteLine("\nSkills using Select (Incorrect for flattening):");
foreach (var skillList in skillsUsingSelect)
{
// This will print the default ToString() of List<string>, not the skills themselves
Console.WriteLine(skillList);
// Example output line: System.Collections.Generic.List`1[System.String]
}الـSelect هترجعلك sequence من نوع List<string> (ليستة الليستات)، مش ليستة واحدة فيها كل الـ strings. عشان كده SelectMany هي المناسبة في حالة الـ Flattening.
2. Ordering Operators (ترتيب العناصر)
الـ Operators دي بنستخدمها عشان نرتب عناصر الـ sequence بناءً على property معينة أو أكتر. (دول Deferred Execution).
OrderBy()
- بترتب العناصر تصاعديًا (Ascending) (من الصغير للكبير، أو أبجديًا للـ
strings).
// Order employees by Salary (ascending)
var sortedBySalaryAsc = employees.OrderBy(emp => emp.Salary);
Console.WriteLine("\nEmployees Sorted by Salary (Ascending):");
foreach (var emp in sortedBySalaryAsc)
{
Console.WriteLine(emp);
}
// Query Syntax equivalent:
var sortedBySalaryAscQuery = from e in employees
orderby e.Salary // 'ascending' is the default
select e;OrderByDescending()
- بترتب العناصر تنازليًا (Descending) (من الكبير للصغير).
// Order employees by Salary (descending)
var sortedBySalaryDesc = employees.OrderByDescending(emp => emp.Salary);
Console.WriteLine("\nEmployees Sorted by Salary (Descending):");
foreach (var emp in sortedBySalaryDesc)
{
Console.WriteLine(emp);
}
// Query Syntax equivalent:
var sortedBySalaryDescQuery = from e in employees
orderby e.Salary descending
select e;ThenBy() / ThenByDescending()
- بنستخدمهم بعد
OrderByأوOrderByDescendingعشان نعمل ترتيب ثانوي (secondary sort). - يعني لو فيه عنصرين ليهم نفس القيمة في الترتيب الأول (مثلًا اتنين موظفين ليهم نفس المرتب)، نستخدم
ThenByعشان نرتبهم تاني بناءً علىpropertyتانية (مثلًا نرتبهم أبجديًا بالاسم).
// Order by Gender (asc), then by Name (asc) for those with the same gender
var sortedByGenderThenName = employees
.OrderBy(emp => emp.Gender)
.ThenBy(emp => emp.Name);
Console.WriteLine("\nEmployees Sorted by Gender (Asc) then Name (Asc):");
foreach (var emp in sortedByGenderThenName)
{
Console.WriteLine(emp);
}
// You can also use ThenByDescending
var sortedBySalaryDescThenNameAsc = employees
.OrderByDescending(emp => emp.Salary) // Highest salary first
.ThenBy(emp => emp.Name); // If salaries are equal, sort by name A-Z
// Query Syntax equivalent (comma separates the sorting criteria):
var sortedQuery = from e in employees
orderby e.Gender, e.Name // Primary sort by Gender, Secondary by Name
select e;
var sortedQuery2 = from e in employees
orderby e.Salary descending, e.Name ascending // Can mix ascending/descending
select e;
Reverse()
- دي مش بتعمل
sortبمعنى الكلمة، هي بس بتعكس ترتيب العناصر في الـsequenceزي ما هي.
// Reverse the original order of the list
// Note: Reverse() modifies the list in-place if it's a List<T>,
// but the LINQ Reverse() extension method returns a new reversed IEnumerable
// without modifying the original source IF the source is not a List/Array.
// For safety, let's apply it to a query result or a copy if needed.
var reversedOrder = employees.AsEnumerable().Reverse(); // Apply to IEnumerable view
Console.WriteLine("\nEmployees in Reversed Original Order:");
foreach (var emp in reversedOrder)
{
Console.WriteLine(emp);
}
// Note: Query Syntax does not have a direct equivalent for Reverse().3. Element Operators (اختيار عنصر محدد)
الـ Operators دي بنستخدمها عشان نجيب عنصر واحد بس من الـ sequence بناءً على موقعه أو شرط معين.
- مهم جدًا: كل الـ
Operatorsفي الـCategoryدي بتعمل Immediate Execution (تنفيذ فوري). - مهم جدًا: معظمهم متاحين بس كـ Fluent Syntax (أو ممكن تستخدم Hybrid Syntax).
الـ Operators الأساسية ومقارنتها:
| Operator | الوصف | لو الـ Sequence فاضية؟ | لو فيه أكتر من عنصر بيحقق الشرط؟ | لو مفيش ولا عنصر بيحقق الشرط؟ |
|---|---|---|---|---|
First() | بيرجع أول عنصر. | Exception | بيرجع أول واحد يقابله | Exception (لو فيه شرط) |
FirstOrDefault() | بيرجع أول عنصر، أو القيمة الافتراضية (null للـ reference types، 0 للـ int، false للـ bool…). | بيرجع Default | بيرجع أول واحد يقابله | بيرجع Default (لو فيه شرط) |
Last() | بيرجع آخر عنصر. | Exception | بيرجع آخر واحد يقابله | Exception (لو فيه شرط) |
LastOrDefault() | بيرجع آخر عنصر، أو القيمة الافتراضية. | بيرجع Default | بيرجع آخر واحد يقابله | بيرجع Default (لو فيه شرط) |
Single() | بيرجع العنصر الوحيد. | Exception | Exception | Exception |
SingleOrDefault() | بيرجع العنصر الوحيد، أو القيمة الافتراضية لو فاضية. | بيرجع Default | Exception | بيرجع Default |
ElementAt(index) | بيرجع العنصر عند فهرس (index) معين (بيبدأ من 0). | Exception (لو index غلط) | N/A | Exception (لو index غلط) |
ElementAtOrDefault(index) | بيرجع العنصر عند فهرس (index) معين، أو القيمة الافتراضية لو الـ index غلط (خارج النطاق). | بيرجع Default (لو index غلط) | N/A | بيرجع Default (لو index غلط) |
أمثلة:
- كل
operatorمن دول (ما عداElementAt/OrDefault) ليهoverloadتاني بياخدpredicate(شرط) عشان يلاقي أول/آخر/وحيد عنصر بيحقق الشرط ده. - الـ
OrDefaultversions (زيFirstOrDefault,LastOrDefault,SingleOrDefault,ElementAtOrDefault) ليهمoverloadتاني (في بعض الـ versions الجديدة من .NET) بيسمحلك تحدد القيمة الـdefaultاللي ترجع بدل الـdefaultبتاع الـtype.
Console.WriteLine("\n--- Element Operators ---");
// First
try
{
var firstEmployee = employees.First(); // Gets the first employee (Ahmed)
Console.WriteLine($"First Employee: {firstEmployee.Name}");
var firstFemale = employees.First(e => e.Gender == "Female"); // Gets the first female (Mona)
Console.WriteLine($"First Female Employee: {firstFemale.Name}");
// var nonExistent = employees.First(e => e.Salary > 10000); // Throws Exception
}
catch (Exception ex) { Console.WriteLine($"First() Exception: {ex.Message}"); }
// FirstOrDefault
var firstEmpOrDefault = employees.FirstOrDefault(); // Gets Ahmed
Console.WriteLine($"FirstOrDefault Employee: {firstEmpOrDefault?.Name}");
var firstFemaleOrDefault = employees.FirstOrDefault(e => e.Gender == "Female"); // Gets Mona
Console.WriteLine($"FirstOrDefault Female: {firstFemaleOrDefault?.Name}");
var nonExistentOrDefault = employees.FirstOrDefault(e => e.Salary > 10000); // Returns null
Console.WriteLine($"FirstOrDefault NonExistent (>10k Salary): {nonExistentOrDefault?.Name ?? "null"}");
// Example with custom default (might require newer .NET or specific package)
// var customDefault = employees.FirstOrDefault(e => e.Salary > 10000, new Employee { Name = "Default Emp" });
// Last
try
{
var lastEmployee = employees.Last(); // Gets the last employee added (depends on list init)
Console.WriteLine($"Last Employee: {lastEmployee.Name}");
var lastMale = employees.Last(e => e.Gender == "Male"); // Gets the last male (Ali)
Console.WriteLine($"Last Male Employee: {lastMale.Name}");
}
catch (Exception ex) { Console.WriteLine($"Last() Exception: {ex.Message}"); }
// LastOrDefault
var lastOrDefaultEmp = employees.LastOrDefault();
Console.WriteLine($"LastOrDefault Employee: {lastOrDefaultEmp?.Name}");
var lastOrDefaultNonExistent = employees.LastOrDefault(e => e.Address == "Mars"); // Returns null
Console.WriteLine($"LastOrDefault NonExistent (Mars): {lastOrDefaultNonExistent?.Name ?? "null"}");
// Single (Use when you expect EXACTLY ONE result)
try
{
// var singleEmp = employees.Single(); // Throws Exception (more than one employee)
var singleAli = employees.Single(e => e.Name == "Ali"); // Gets Ali (assuming name is unique)
Console.WriteLine($"Single Employee (Ali): {singleAli.Name}");
// var singleFemale = employees.Single(e => e.Gender == "Female"); // Throws Exception (more than one female)
// var singleNonExistent = employees.Single(e => e.Salary > 10000); // Throws Exception (none found)
}
catch (Exception ex) { Console.WriteLine($"Single() Exception: {ex.Message}"); }
// SingleOrDefault (Use when you expect ZERO or ONE result)
try
{
// var singleOrDefaultEmp = employees.SingleOrDefault(); // Throws Exception (more than one employee)
var singleOrDefaultAli = employees.SingleOrDefault(e => e.Name == "Ali"); // Gets Ali
Console.WriteLine($"SingleOrDefault Employee (Ali): {singleOrDefaultAli?.Name}");
var singleOrDefaultNonExistent = employees.SingleOrDefault(e => e.Salary > 10000); // Returns null (zero found)
Console.WriteLine($"SingleOrDefault NonExistent (>10k Salary): {singleOrDefaultNonExistent?.Name ?? "null"}");
// var singleOrDefaultFemale = employees.SingleOrDefault(e => e.Gender == "Female"); // Throws Exception (more than one female)
}
catch (Exception ex) { Console.WriteLine($"SingleOrDefault() Exception: {ex.Message}"); }
// ElementAt
try
{
var secondEmployee = employees.ElementAt(1); // Gets the employee at index 1 (Mona)
Console.WriteLine($"ElementAt(1): {secondEmployee.Name}");
// var outOfBounds = employees.ElementAt(100); // Throws Exception
}
catch (Exception ex) { Console.WriteLine($"ElementAt() Exception: {ex.Message}"); }
// ElementAtOrDefault
var secondEmpOrDefault = employees.ElementAtOrDefault(1); // Gets Mona
Console.WriteLine($"ElementAtOrDefault(1): {secondEmpOrDefault?.Name}");
var outOfBoundsOrDefault = employees.ElementAtOrDefault(100); // Returns null
Console.WriteLine($"ElementAtOrDefault(100): {outOfBoundsOrDefault?.Name ?? "null"}");4. Aggregate Operators (تجميع وحساب)
الـ Operators دي بتشتغل على الـ sequence كلها عشان ترجع قيمة واحدة كنتيجة لعملية حسابية أو تجميعية. أشهرهم 7 تقريبًا. (دول Immediate Execution).
| Operator | الوصف |
|---|---|
Sum() | بيرجع مجموع قيم رقمية في الـ sequence. |
Min() | بيرجع أقل قيمة في الـ sequence. |
Max() | بيرجع أعلى قيمة في الـ sequence. |
Average() | بيرجع المتوسط الحسابي لقيم رقمية. |
Count() | بيرجع عدد العناصر في الـ sequence. |
LongCount() | زي Count بس بيرجع long (لو العدد كبير أوي). |
Aggregate() | بيعمل عملية تجميعية مخصصة أنت بتعرفها (زي تجميع string أو عملية حسابية معقدة). |
MaxBy() | (.NET 6+) بيرجع العنصر نفسه اللي ليه أعلى قيمة لـ property معينة. |
MinBy() | (.NET 6+) بيرجع العنصر نفسه اللي ليه أقل قيمة لـ property معينة. |
Any() | (Boolean) بيرجع true لو فيه عنصر واحد على الأقل بيحقق شرط معين (أو لو مش فاضية). |
All() | (Boolean) بيرجع true لو كل العناصر بتحقق شرط معين. |
أمثلة:
Console.WriteLine("\n--- Aggregate Operators ---");
// Sum (Needs a numeric property)
var totalSalary = employees.Sum(e => e.Salary);
Console.WriteLine($"Total Salary: {totalSalary}");
// Min (Needs a comparable property)
var minSalary = employees.Min(e => e.Salary);
Console.WriteLine($"Minimum Salary: {minSalary}");
// Max
var maxSalary = employees.Max(e => e.Salary);
Console.WriteLine($"Maximum Salary: {maxSalary}");
// Average
var averageSalary = employees.Average(e => e.Salary);
Console.WriteLine($"Average Salary: {averageSalary:F2}"); // F2 for formatting
// Count
var employeeCount = employees.Count(); // Total number of employees
Console.WriteLine($"Total Employees: {employeeCount}");
var maleCount = employees.Count(e => e.Gender == "Male"); // Number of male employees
Console.WriteLine($"Male Employees: {maleCount}");
// Any (Checks for existence)
bool hasHighEarner = employees.Any(e => e.Salary > 7000); // Is there anyone earning > 7000?
Console.WriteLine($"Any employee earns > 7000? {hasHighEarner}");
bool listIsNotEmpty = employees.Any(); // Is the list not empty?
Console.WriteLine($"Is the list not empty? {listIsNotEmpty}");
// All (Checks if all elements satisfy a condition)
bool allEarnAbove3000 = employees.All(e => e.Salary > 3000); // Does everyone earn > 3000?
Console.WriteLine($"All employees earn > 3000? {allEarnAbove3000}");
// Aggregate (Example: Concatenate all names)
string allNames = employees
.Select(e => e.Name) // First select the names
.Aggregate((currentNames, nextName) => currentNames + ", " + nextName); // Aggregate them
Console.WriteLine($"All Names Concatenated: {allNames}");
// Aggregate Example: Calculate product of salaries (just for demo, not meaningful)
// Need to handle potential zero salary if calculating product
long salaryProduct = employees.Select(e => (long)e.Salary).Aggregate(1L, (acc, salary) => acc * (salary == 0 ? 1 : salary)); // Start with 1L (long)
Console.WriteLine($"Salary Product (Example): {salaryProduct}");
// MaxBy / MinBy (.NET 6+)
// Find the employee object with the highest salary
var highestEarner = employees.MaxBy(e => e.Salary);
Console.WriteLine($"Highest Earning Employee: {highestEarner?.Name} ({highestEarner?.Salary})");
// Find the employee object with the lowest salary
var lowestEarner = employees.MinBy(e => e.Salary);
Console.WriteLine($"Lowest Earning Employee: {lowestEarner?.Name} ({lowestEarner?.Salary})");
ملاحظات على Sum, Min, Max, Average:
- لو استخدمتهم على
sequenceمنobjects(زيemployees) من غير ما تحدد الـpropertyاللي هيشتغلوا عليها (يعني تقولemployees.Sum()بس)، الـclassبتاع الـobjectده (اللي هوEmployee) لازم يكون بيعملimplementلـinterfaceزي Cs ICompareable عشان الـoperatorيعرف يقارن أو يجمع على أساس إيه. بس الطريقة دي مش عملية ومش بنستخدمها غالبًا. - الأحسن دايمًا إنك تحدد الـ
propertyاللي عايز تعمل عليها العملية باستخدامlambda expression(زيemployees.Sum(e => e.Salary)). - ليهم
overloadsكتير عشان يتعاملوا مع أنواع أرقام مختلفة (int,double,decimal,long,Nullable versions, etc.).
مقارنة Max/Min بـ MaxBy/MinBy:
- الـ
Max(e => e.Salary): بترجع أعلى قيمة للمرتب نفسها (مثلًا 8000). - الـ
MaxBy(e => e.Salary): بترجع الموظف نفسه (Employee object) اللي عنده أعلى مرتب. - الـ
MaxBy/MinByمفيدين لما تكون عايز الـobjectكله مش بس القيمة. (متاحين من .NET 6). - قبل .NET 6، كنا بنعمل نفس وظيفة
MaxByعن طريقOrderByDescending(...).First(). - مهم لـ EF Core:
MaxBy/MinByممكن يكونوا لسه مش مترجمين كويس لـSQLفي كل إصداراتEF Core، لكن الدعم بيتحسن.
مقارنة Count (الـ LINQ Operator) بـ Count (الـ Property):
- الـ
List<T>.Count(Property): ديpropertyموجودة فيListوArrayوبعض الـcollectionsالتانية. بترجع عدد العناصر بسرعة جدًا لإن العدد بيكون متخزن. ما ينفعش تستخدمها معIEnumerableعامةً (لإنIEnumerableمش بيضمن إن العدد معروف مسبقًا). - الـ
.Count()(LINQ Operator/Extension Method): ديmethodبتشتغل مع أيIEnumerable.- لو الـ
IEnumerableده أصلًاListأوArray، الـmethodدي بتكون ذكية كفاية إنها تستخدم الـCount/Lengthproperty عشان تجيب العدد بسرعة. - لو الـ
IEnumerableده نوع تاني (زي نتيجةqueryلسه منفذتش)، الـmethodدي بتضطر تلف على كل العناصر عشان تعدهم (وده ممكن يكون أبطأ لو الـsequenceكبيرة). - الـ
Operatorليهoverloadبياخدpredicateعشان يعد العناصر اللي بتحقق شرط معين (employees.Count(e => e.Gender == "Male")). الـPropertyطبعًا ما ينفعش تعمل كده.
- لو الـ
إمتى أستخدم Any() بدل Count() > 0؟
- لو كل اللي يهمك تعرفه هو “هل فيه أي عنصر بيحقق الشرط ده؟” أو “هل الليستة دي مش فاضية؟”، دايمًا استخدم
Any(). - الـ
Any()أسرع بكتير منCount() > 0في معظم الحالات (خصوصًا معdatabasesأوsequencesمشList/Array). - ليه؟ لإن
Any()بتقف أول ما تلاقي أول عنصر بيحقق الشرط (أو أول عنصر لو مفيش شرط). - الـ
Count()(لو مشList/Array) بتضطر تلف على كل العناصر عشان تجيب العدد الكلي، وبعدين تقارنه بالصفر. - متستخدمش
Count()غير لو أنت فعلًا محتاج العدد نفسه لسبب ما. - اتكلمنا عن Cs Aggregate بالتفصيل هنا
5. Casting / Conversion Operators (تحويل النوع)
الـ Operators دي بنستخدمها عشان نحول نتيجة LINQ query (اللي غالبًا بتكون IEnumerable) لنوع collection معين ومشهور زي List أو Array أو Dictionary. (دول Immediate Execution).
ToList()
- بتحول الـ
sequenceلـList<T>. دي أكتر واحدة بنستخدمها غالبًا عشان نجبر الـqueryإنه يتنفذ وناخد النتائج فيList.
IEnumerable<Employee> query = employees.Where(e => e.Salary > 5000); // Deferred
List<Employee> highEarnersList = query.ToList(); // Immediate Execution
Console.WriteLine("\nHigh Earners List (ToList):");
highEarnersList.ForEach(e => Console.WriteLine(e.Name)); // Can use List methods nowToArray()
- بتحول الـ
sequenceلـArray(T[]).
Employee[] highEarnersArray = employees.Where(e => e.Salary > 5000).ToArray(); // Immediate
Console.WriteLine("\nHigh Earners Array (ToArray):");
foreach(var emp in highEarnersArray) { Console.WriteLine(emp.Name); }ToDictionary()
- بتحول الـ
sequenceلـDictionary<TKey, TValue>. - بتحتاج تحدد لها حاجتين بـ
lambda expressions:- الـ
keySelector: ازاي تجيب الـKeyبتاع الـDictionaryمن كل عنصر في الـsequence. الـKeyلازم يكونunique. - الـ
elementSelector(اختياري): ازاي تجيب الـValueبتاع الـDictionary. لو محددتوش، الـValueهيكون العنصر نفسه.
- الـ
try
{
// Create a dictionary where Key is Employee Id, Value is Employee Name
Dictionary<int, string> employeeNameDict = employees
.ToDictionary(
emp => emp.Id, // Key selector: Use employee Id as the key
emp => emp.Name // Element selector: Use employee Name as the value
);
Console.WriteLine("\nEmployee Dictionary (Id -> Name):");
foreach (var kvp in employeeNameDict)
{
Console.WriteLine("Key: {kvp.Key}, Value: {kvp.Value}");
}
// Example where Value is the employee object itself
Dictionary<int, Employee> employeeDict = employees.ToDictionary(emp => emp.Id); // Element selector omitted
Console.WriteLine($"\nEmployee with Id 2 from Dictionary: {employeeDict[2].Name}");
// This will throw an exception if keys are not unique
// Dictionary<string, Employee> dictByGender = employees.ToDictionary(emp => emp.Gender); // Throws Exception
}
catch (Exception ex)
{
Console.WriteLine($"ToDictionary() Exception: {ex.Message}");
}
ToHashSet()
- بتحول الـ
sequenceلـHashSet<T>. - الـ
HashSetبيضمن إن العناصر تكونunique(مفيهوش تكرار). - متاح من .NET 6+.
// Example: Get a set of unique genders
HashSet<string> genders = employees.Select(e => e.Gender).ToHashSet();
Console.WriteLine("\nUnique Genders (ToHashSet):");
Console.WriteLine(string.Join(", ", genders)); // Output: Male, Female (order not guaranteed)ToLookup()
- بتحول الـ
sequenceلـILookup<TKey, TElement>. - الـ
Lookupشبه الـDictionary، بس بيسمح بوجود أكتر من عنصر لنفس الـKey. الـValueبيكون عبارة عنIEnumerableبالعناصر اللي ليها نفس الـKey. - مفيد جدًا لو عايز تعمل
groupingللداتا بس في شكل تقدر توصل له بالـkeyبسرعة.
// Create a Lookup grouping employees by Gender
ILookup<string, Employee> employeesByGender = employees.ToLookup(emp => emp.Gender);
Console.WriteLine("\nEmployees Lookup by Gender (ToLookup):");
// Access employees for a specific key ("Male")
Console.WriteLine("Male Employees:");
if (employeesByGender.Contains("Male")) // Good practice to check if key exists
{
foreach (var emp in employeesByGender["Male"]) // Access the IEnumerable<Employee> for the key
{
Console.WriteLine($"- {emp.Name}");
}
}
// Access employees for another key ("Female")
Console.WriteLine("Female Employees:");
foreach (var emp in employeesByGender["Female"])
{
Console.WriteLine($"- {emp.Name}");
}
// Access a non-existent key (doesn't throw exception, returns empty sequence)
Console.WriteLine("Employees with Gender 'Other':");
foreach (var emp in employeesByGender["Other"]) // This loop won't execute
{
Console.WriteLine($"- {emp.Name}");
}6. Generation Operators (إنشاء Sequences)
الـ Operators دي مختلفة عن اللي فاتوا، لإنها مش بتشتغل على input sequence موجودة، هي نفسها بتنشئ (generate) sequence جديدة. (دول Deferred Execution، بس بينشئوا sequence تقدر تعمل عليها loop).
لازم نناديهم كـ Static Methods من الـ Enumerable class.
Enumerable.Range(start, count)
- بتنشئ
sequenceمن الأرقام الصحيحة المتسلسلة. - الـ
start: الرقم اللي هتبدأ بيه. - الـ
count: عدد الأرقام اللي عايز تنشئها.
// Generate numbers from 5 to 9 (start=5, count=5)
IEnumerable<int> numberRange = Enumerable.Range(5, 5); // 5, 6, 7, 8, 9
Console.WriteLine("\nGenerated Range (Range):");
Console.WriteLine(string.Join(", ", numberRange));Enumerable.Repeat(element, count)
- بتنشئ
sequenceعن طريق تكرار قيمة معينة عدد معين من المرات. - الـ
element: القيمة اللي عايز تكررها. - الـ
count: عدد مرات التكرار.
// Repeat the string "Hi" 4 times
IEnumerable<string> repeatedHi = Enumerable.Repeat("Hi", 4); // "Hi", "Hi", "Hi", "Hi"
Console.WriteLine("\nGenerated Repetition (Repeat):");
Console.WriteLine(string.Join(", ", repeatedHi));
// Repeat the number 0 five times
var zeros = Enumerable.Repeat(0, 5); // 0, 0, 0, 0, 0
Console.WriteLine(string.Join(", ", zeros));
Enumerable.Empty<T>()
- بتنشئ
sequenceفاضية من نوع معينT. - مفيدة أحيانًا لو عايز ترجع
sequenceفاضية منmethodبدل ما ترجعnull.
// Create an empty sequence of Products
IEnumerable<Product> emptyProducts = Enumerable.Empty<Product>();
Console.WriteLine($"\nIs emptyProducts sequence empty? {!emptyProducts.Any()}"); // True7. Set Operators (عمليات المجموعات)
الـ Operators دي بتتعامل مع الـ sequences كأنها sets (مجموعات)، وبتسمح لك تعمل عمليات زي الاتحاد والتقاطع والفرق بين مجموعتين، أو تشيل التكرار من مجموعة واحدة. (دول Deferred Execution).
نقطة مهمة: مقارنة العناصر (Overriding Equals & GetHashCode)
- لما الـ
Set Operatorsدي بتقارن بين عنصرين عشان تعرف هل هما متساويين (عشان مثلًا تشيل التكرار فيDistinctأوUnion)، هي بتستخدم طريقة المقارنة الافتراضية. - لو العناصر دي
primitive types(زيint,string,bool)، المقارنة بتكون على القيمة نفسها، وده غالبًا اللي إحنا عايزينه. - لكن لو العناصر دي
objectsمنclassإحنا عاملينه (زيEmployeeأوProduct)، المقارنة الافتراضية بتكون بمقارنة الـ Reference (عنوان الـ object في الذاكرة)، مش بمقارنة قيم الـpropertiesاللي جواه. ده معناه إن اتنينEmployee objectsليهم نفس الـIdوالـNameهيعتبروا مختلفين لو هماinstancesمنفصلة. - الحل: لو عايز الـ
Set Operatorsتقارن الـobjectsبتاعتك بناءً على قيم الـproperties، لازم تعملoverrideلاتنينmethodsمهمين جوه الـclassبتاعك (اللي ورثتهم من Cs System.Object وكنا اتكلمنا عنها في Cs Equality):- الـ
Equals(object obj): تعرف فيها إمتى تعتبر اتنينobjectsمن الـclassده متساويين (مثلًا لو ليهم نفس الـId). - الـ
GetHashCode(): لازم كمان تعملهاoverrideبحيث إن أي اتنينobjectsمتساويين حسبEquals، لازم يكون ليهم نفس الـHashCode. لو معملتشoverrideلـGetHashCodeصح، الـSet Operators(وكمانDictionaryوHashSet) ممكن تشتغل غلط.- ليه بنعمل
overrideللاتنين مع بعض؟ لإن الـSet OperatorsوالـCollectionsاللي بتعتمد علىhash(زيHashSet,Dictionary) بتستخدمGetHashCodeالأول كطريقة سريعة عشان تقسم العناصر لمجموعات صغيرة (buckets)، وبعدين جوه الـbucketالواحد بس بتقارن باستخدامEquals. لو اتنينobjectsمتساويين بس الـHashCodeبتاعهم مختلف، ممكن يتحطوا فيbucketsمختلفة وميتقارنوش بـEqualsأصلًا. - إزاي نعمل
overrideلـGetHashCode؟ الطريقة الشائعة إنك تختار الـpropertiesالأساسية اللي بتعرف الـequality(زي الـId) وتدمج الـHashCodesبتاعتهم مع بعض باستخدام عملية زيXOR (^)أو باستخدامHashCode.Combine()(في .NET الأحدث).
- ليه بنعمل
- الـ
// Example inside Product class (assuming equality is based on Id only)
public class Product // : IEquatable<Product> // Optional: Implement IEquatable for better performance
{
public int Id { get; set; }
public string Name { get; set; }
public string Category { get; set; }
public double Price { get; set; }
public override string ToString() => $"Id: {Id}, Name: {Name}, Category: {Category}, Price: {Price}";
// --- Overriding Equals and GetHashCode ---
public override bool Equals(object obj)
{
// Basic implementation comparing by Id
if (obj == null || GetType() != obj.GetType())
{
return false;
}
Product other = (Product)obj;
return Id == other.Id;
// OR use pattern matching (C# 7+)
// return obj is Product other && this.Id == other.Id;
}
// Must override GetHashCode if Equals is overridden
public override int GetHashCode()
{
// Basic implementation using Id's hash code
// If using multiple properties for Equals, combine their hash codes
// return HashCode.Combine(Id, Name, Category); // Example for multiple properties (.NET Core+)
return Id.GetHashCode();
}
// --- End Overrides ---
// Optional: Implement IEquatable<T>
// public bool Equals(Product other)
// {
// if (other == null) return false;
// return this.Id == other.Id;
// }
}
- بعد ما تعمل
overrideصح، الـSet Operatorsهتشتغل زي ما إنت متوقع على الـobjectsبتاعتك. - بديل: لو مش عايز تعمل
overrideفي الـclassنفسه، معظم الـSet Operatorsليهاoverloadتاني بياخدparameterمن نوعIEqualityComparer<T>. دهinterfaceبتعمل لهimplementفيclassمنفصل، وبتعرف فيه الـEqualsوالـGetHashCodeاللي إنت عايز تستخدمهم للمقارنة دي بس.
Distinct()
- بتشيل العناصر المكررة من
sequenceواحدة. بترجعsequenceفيها العناصر الـuniqueبس.
List<int> numbersWithDuplicates = new List<int> { 1, 2, 2, 3, 1, 4, 5, 4 };
var distinctNumbers = numbersWithDuplicates.Distinct(); // 1, 2, 3, 4, 5
Console.WriteLine("\nDistinct Numbers:");
Console.WriteLine(string.Join(", ", distinctNumbers));
// Example with Products (Needs Equals/GetHashCode override based on Id)
List<Product> productListDups = new List<Product>
{
new Product { Id = 101, Name = "Laptop", Category = "Devices", Price = 25000 },
new Product { Id = 102, Name = "Keyboard", Category = "Devices", Price = 1500 },
new Product { Id = 101, Name = "Laptop New", Category = "Devices", Price = 26000 }, // Duplicate Id
};
var distinctProducts = productListDups.Distinct(); // Will keep only one product with Id 101
Console.WriteLine("\nDistinct Products (by Id):");
foreach(var p in distinctProducts) { Console.WriteLine(p); }Union()
- بتجيب اتحاد مجموعتين (
sequence1 +sequence2)، مع إزالة العناصر المكررة بين المجموعتين أو جوه كل مجموعة.
List<int> listA = new List<int> { 1, 2, 3, 4 };
List<int> listB = new List<int> { 3, 4, 5, 6 };
var unionResult = listA.Union(listB); // 1, 2, 3, 4, 5, 6
Console.WriteLine("\nUnion of listA and listB:");
Console.WriteLine(string.Join(", ", unionResult));Concat()
- بتدمج (Concatenate) مجموعتين ورا بعض من غير ما تشيل أي تكرار. بترجع كل عناصر الأولى وبعدها كل عناصر التانية.
- دي زي
UNION ALLفيSQL.
var concatResult = listA.Concat(listB); // 1, 2, 3, 4, 3, 4, 5, 6
Console.WriteLine("\nConcatenation of listA and listB:");
Console.WriteLine(string.Join(", ", concatResult));
// Note: listA.Union(listB) is equivalent to listA.Concat(listB).Distinct()Intersect()
- بتجيب تقاطع مجموعتين، يعني العناصر المشتركة الموجودة في المجموعتين الاتنين.
var intersectResult = listA.Intersect(listB); // 3, 4
Console.WriteLine("\nIntersection of listA and listB:");
Console.WriteLine(string.Join(", ", intersectResult));Except()
- بتجيب الفرق بين مجموعتين. بترجع العناصر الموجودة في المجموعة الأولى ومش موجودة في المجموعة التانية.
var exceptResult = listA.Except(listB); // 1, 2 (Elements in A that are not in B)
Console.WriteLine("\nElements in listA Except those in listB:");
Console.WriteLine(string.Join(", ", exceptResult));
var exceptResultReverse = listB.Except(listA); // 5, 6 (Elements in B that are not in A)
Console.WriteLine("\nElements in listB Except those in listA:");
Console.WriteLine(string.Join(", ", exceptResultReverse));الـ ...By Operators (.NET 6+)
زي ما فيه MaxBy/MinBy، فيه كمان overloads جديدة للـ Set Operators دي من أول .NET 6 بتنتهي بـ By (زي DistinctBy, UnionBy, IntersectBy, ExceptBy).
- فايدتهم: بيخلّوك تحدد
key selector(يعنيlambda expressionبتختارpropertyمعينة) عشان المقارنة تتم بناءً على الـkeyده بس، بدل ما تحتاج تعملoverrideلـEquals/GetHashCodeأو تستخدمIEqualityComparer.
مثال: نجيب المنتجات الـ unique بناءً على الـ Category بس:
// Using DistinctBy ( .NET 6+) to get one product per category
var distinctByCategory = products.DistinctBy(p => p.Category);
Console.WriteLine("\nDistinct Products by Category (DistinctBy):");
foreach (var p in distinctByCategory)
{
Console.WriteLine(p); // Gets Laptop (Devices), T-Shirt (Clothing) - or others depending on which comes first
}
// Example: Union products based on Category key
List<Product> moreProducts = new List<Product> {
new Product { Id = 201, Name = "Jeans", Category = "Clothing", Price = 1200 },
new Product { Id = 202, Name = "Monitor", Category = "Devices", Price = 5000 }
};
var unionByCategory = products.UnionBy(moreProducts, p => p.Category);
Console.WriteLine("\nUnion By Category:");
foreach (var p in unionByCategory) { Console.WriteLine(p); } // Shows one product per unique category from both listsالـ ...By operators دي بتبسط الدنيا جدًا لما تكون عايز تعمل مقارنة بناءً على property واحدة أو أكتر من غير ما تعدل الـ class الأصلي.
8. Quantifier Operators (تحديد الكمية - Boolean)
الـ Operators دي بتشتغل على الـ sequence كلها عشان ترجع قيمة boolean (true أو false) بتجاوب على سؤال عن كمية العناصر اللي بتحقق شرط معين. (دول ليهم سلوك Immediate Execution في الغالب لإنهم لازم يوصلوا لنتيجة true/false).
Any()
- بترجع
trueلو فيه عنصر واحد على الأقل في الـsequenceبيحقق الشرط اللي مديهولها. - لو من غير شرط (
Any())، بترجعtrueلو الـsequenceمش فاضية (فيها عنصر واحد على الأقل). - بتقف أول ما تلاقي أول عنصر يحقق الشرط (عشان كده سريعة).
bool anyMales = employees.Any(e => e.Gender == "Male"); // true
Console.WriteLine($"\nAny males? {anyMales}");
bool anySalaryOver9k = employees.Any(e => e.Salary > 9000); // false
Console.WriteLine($"Any salary > 9k? {anySalaryOver9k}");
bool isEmployeeListNotEmpty = employees.Any(); // true
Console.WriteLine($"Is employees list not empty? {isEmployeeListNotEmpty}");
List<int> emptyList = new List<int>();
bool isEmptyListNotEmpty = emptyList.Any(); // false
Console.WriteLine($"Is emptyList not empty? {isEmptyListNotEmpty}");All()
- بترجع
trueلو كل العناصر في الـsequenceبيحققوا الشرط اللي مديهولها. - لو الـ
sequenceفاضية، بترجعtrue(لإنه مفيش ولا عنصر مقدرش يحقق الشرط!). - دي زي الـ TrueForAll in List
bool allEarnMoreThan4k = employees.All(e => e.Salary > 4000); // true (assuming sample data)
Console.WriteLine($"\nAll earn > 4k? {allEarnMoreThan4k}");
bool allAreMale = employees.All(e => e.Gender == "Male"); // false
Console.WriteLine($"All are male? {allAreMale}");
bool allSatisfyInEmpty = emptyList.All(x => x > 5); // true
Console.WriteLine($"All satisfy condition in empty list? {allSatisfyInEmpty}");Contains()
- بترجع
trueلو الـsequenceبتحتوي على عنصر معين أنت بتدور عليه. - بتحتاج تعمل
overrideلـEquals/GetHashCodeلو بتدور علىobjectمركب. - فيه
overloadبياخدIEqualityComparer.
List<string> letters = new List<string> { "A", "B", "C" };
bool hasB = letters.Contains("B"); // true
Console.WriteLine($"\nLetters list contains 'B'? {hasB}");
bool hasX = letters.Contains("X"); // false
Console.WriteLine($"Letters list contains 'X'? {hasX}");
// Example with objects (needs Equals/GetHashCode override)
Product laptop = new Product { Id = 101, Name = "Laptop", Category = "Devices", Price = 25000 };
bool productListHasLaptop = products.Contains(laptop); // true (because we overrode Equals based on Id)
Console.WriteLine($"Products list contains laptop (Id 101)? {productListHasLaptop}");SequenceEqual()
- بتقارن بين اتنين
sequencesعشان تشوف هل هما متساويتين. - بتكون متساوية لو:
- ليهم نفس عدد العناصر.
- كل عنصر في الـ
sequenceالأولى بيساوي العنصر المقابل له في نفس الـindexفي الـsequenceالتانية (باستخدامEqualsالافتراضي أوoverrideأوIEqualityComparer).
List<int> seq1 = new List<int> { 1, 2, 3 };
List<int> seq2 = new List<int> { 1, 2, 3 };
List<int> seq3 = new List<int> { 1, 3, 2 }; // Different order
List<int> seq4 = new List<int> { 1, 2 }; // Different count
bool seq1EqualsSeq2 = seq1.SequenceEqual(seq2); // true
Console.WriteLine($"\nSequenceEqual(seq1, seq2)? {seq1EqualsSeq2}");
bool seq1EqualsSeq3 = seq1.SequenceEqual(seq3); // false (order matters)
Console.WriteLine($"SequenceEqual(seq1, seq3)? {seq1EqualsSeq3}");
bool seq1EqualsSeq4 = seq1.SequenceEqual(seq4); // false (count matters)
Console.WriteLine($"SequenceEqual(seq1, seq4)? {seq1EqualsSeq4}");
9. Zipping Operator (الدمج بالترتيب)
عندنا operator واحد هنا هو Zip(). (ده Deferred Execution).
Zip()
- بيدمج عنصر عنصر من اتنين
sequencesمع بعض بناءً على الترتيب (index). - بياخد
lambda expressionبتحدد إزاي تدمج العنصر من الـsequenceالأولى مع العنصر المقابل له في الـsequenceالتانية عشان تنتج عنصر جديد في الـresult sequence. - العملية بتقف أول ما أقصر
sequenceمن الاتنين تخلص. حجم الـresult sequenceبيكون بحجم أقصرsequence.
List<string> studentNames = new List<string> { "Amr", "Fatma", "Hassan", "Nour" };
List<int> studentScores = new List<int> { 85, 92, 78 }; // Shorter list
// Zip names and scores together
var studentInfo = studentNames.Zip(studentScores,
(name, score) => $"{name} scored {score}");
// result sequence will have 3 elements only
Console.WriteLine("\nZipped Student Info (Zip):");
foreach (var info in studentInfo)
{
Console.WriteLine(info);
}
// Output:
// Amr scored 85
// Fatma scored 92
// Hassan scored 78
// (Nour is ignored because studentScores ended)10. Grouping Operators (التجميع)
الـ Operator الأساسي هنا هو GroupBy(). بنستخدمه عشان نجمع عناصر الـ sequence في مجموعات بناءً على key معين. (ده Deferred Execution).
GroupBy()
- بياخد
lambda expressionبتحدد الـkeyاللي هيتم التجميع على أساسه (مثلًا نجمع الموظفين حسب الـDepartment). - بيرجع
sequenceجديدة، كل عنصر فيها عبارة عنgroup. - كل
groupبيكون ليها حاجتين أساسيتين:- الـ
Key: القيمة اللي تم التجميع بيها (مثلًا اسم القسم “HR”). - الـ
IEnumerable<T>: مجموعة العناصر الأصلية اللي بتنتمي للـgroupدي (مثلًا كل الموظفين اللي في قسم “HR”). الـgroupنفسها بتكون من نوعIGrouping<TKey, TElement>.
- الـ
- فيه ناس بتفضل تكتب
GroupByبالـ Query Syntax لإنها ممكن تكون أوضح شوية مع كلمةgroup by.
مثال: تجميع المنتجات حسب الـ Category:
Console.WriteLine("\n--- Grouping Operators ---");
// Group products by Category using Query Syntax (often clearer for grouping)
var productsByCategoryQuery = from p in products
group p by p.Category; // Group product 'p' by its 'Category' property
Console.WriteLine("Products Grouped by Category (Query Syntax):");
// productsByCategoryQuery is IEnumerable<IGrouping<string, Product>>
foreach (var categoryGroup in productsByCategoryQuery)
{
Console.WriteLine($"Category: {categoryGroup.Key}"); // The Key is the Category name
// categoryGroup itself is an IEnumerable<Product> for this category
foreach (var product in categoryGroup)
{
Console.WriteLine($"- {product.Name} ({product.Price})");
}
}
// Group products by Category using Fluent Syntax
var productsByCategoryFluent = products.GroupBy(p => p.Category);
Console.WriteLine("\nProducts Grouped by Category (Fluent Syntax):");
foreach (var categoryGroup in productsByCategoryFluent)
{
Console.WriteLine($"Category: {categoryGroup.Key}");
foreach (var product in categoryGroup)
{
Console.WriteLine($"- {product.Name} ({product.Price})");
}
}استخدام into مع group by في الـ Query Syntax:
زي ما استخدمنا into مع join عشان نعمل Left Join، ممكن نستخدمها مع group by عشان نكمل الـ query على الـ groups اللي اتكونت. بنستخدم into عشان ندي اسم للـ group variable ونقدر نعمل عليه where أو orderby أو select بعد كده.
مثال: هات الـ Categories اللي فيها أكتر من منتج واحد (Count > 1)، ورتبهم أبجديًا، واعرض اسم الـ Category وعدد المنتجات فيها.
// Using Query Syntax with 'into' after 'group by'
var categoriesWithMoreThanOneProduct =
from p in products // Start with products
group p by p.Category // Group them by category
into categoryGroup // Put the resulting group into 'categoryGroup' variable
where categoryGroup.Count() > 1 // Filter the groups (keep only those with count > 1)
orderby categoryGroup.Key // Order the remaining groups by category name (the key)
select new // Select the desired output
{
CategoryName = categoryGroup.Key,
ProductCount = categoryGroup.Count()
};
Console.WriteLine("\nCategories with more than 1 product (Query Syntax with into):");
foreach (var catInfo in categoriesWithMoreThanOneProduct)
{
Console.WriteLine($"{catInfo.CategoryName}: {catInfo.ProductCount} products");
}
// Equivalent using Fluent Syntax (chaining methods)
var categoriesWithMoreThanOneProductFluent = products
.GroupBy(p => p.Category) // Group
.Where(categoryGroup => categoryGroup.Count() > 1) // Filter groups
.OrderBy(categoryGroup => categoryGroup.Key) // Order groups
.Select(categoryGroup => new // Select output
{
CategoryName = categoryGroup.Key,
ProductCount = categoryGroup.Count()
});
Console.WriteLine("\nCategories with more than 1 product (Fluent Syntax):");
foreach (var catInfo in categoriesWithMoreThanOneProductFluent)
{
Console.WriteLine($"{catInfo.CategoryName}: {catInfo.ProductCount} products");
}11. Partitioning Operators (تقسيم الـ Sequence)
الـ Operators دي بنستخدمها عشان ناخد جزء (partition) معين من الـ sequence، وغالبًا بنستخدمها في Pagination (عرض الداتا صفحة صفحة). (دول Deferred Execution).
Take(count)
- بياخد أول
countعنصر من بداية الـsequence.
List<int> nums = Enumerable.Range(1, 10).ToList(); // 1 to 10
var firstThree = nums.Take(3); // 1, 2, 3
Console.WriteLine($"\nTake(3): {string.Join(", ", firstThree)}");TakeLast(count) (.NET Standard 2.1+)
- بياخد آخر
countعنصر من نهاية الـsequence.
var lastTwo = nums.TakeLast(2); // 9, 10
Console.WriteLine($"TakeLast(2): {string.Join(", ", lastTwo)}");Skip(count)
- بيتخطى (skip) أول
countعنصر، وبيرجع باقي الـsequence.
var skipFirstThree = nums.Skip(3); // 4, 5, 6, 7, 8, 9, 10
Console.WriteLine($"Skip(3): {string.Join(", ", skipFirstThree)}");SkipLast(count) (.NET Standard 2.1+)
- بيتخطى آخر
countعنصر، وبيرجع الجزء الأولاني من الـsequence.
var skipLastTwo = nums.SkipLast(2); // 1, 2, 3, 4, 5, 6, 7, 8
Console.WriteLine($"SkipLast(2): {string.Join(", ", skipLastTwo)}");TakeWhile(predicate)
- بياخد عناصر من بداية الـ
sequenceطول ما الشرط (predicate) متحقق. - بتقف تمامًا أول ما تلاقي أول عنصر مش بيحقق الشرط. حتى لو العناصر اللي بعده بتحقق الشرط تاني، مش بتاخدهم.
- ليه
overloadبياخد الـindexكمان (TakeWhile((element, index) => ...)).
int[] numbersMixed = { 1, 2, 3, 6, 7, 2, 8, 9 };
var takeWhileLessThan5 = numbersMixed.TakeWhile(n => n < 5); // 1, 2, 3 (stops at 6)
Console.WriteLine($"\nTakeWhile(n < 5): {string.Join(", ", takeWhileLessThan5)}");
var takeWhileIndexLessThan3 = numbersMixed.TakeWhile((n, index) => index < 3); // 1, 2, 3
Console.WriteLine($"TakeWhile(index < 3): {string.Join(", ", takeWhileIndexLessThan3)}");SkipWhile(predicate)
- بيتخطى عناصر من بداية الـ
sequenceطول ما الشرط (predicate) متحقق. - بتبدأ تاخد العناصر أول ما تلاقي أول عنصر مش بيحقق الشرط، وبتاخد العنصر ده وكل اللي بعده (من غير ما تعمل check للشرط تاني).
- ليه
overloadبياخد الـindexكمان.
var skipWhileLessThan5 = numbersMixed.SkipWhile(n => n < 5); // 6, 7, 2, 8, 9 (skips 1, 2, 3, starts taking from 6)
Console.WriteLine($"\nSkipWhile(n < 5): {string.Join(", ", skipWhileLessThan5)}");
var skipWhileIndexLessThan3 = numbersMixed.SkipWhile((n, index) => index < 3); // 6, 7, 2, 8, 9
Console.WriteLine($"SkipWhile(index < 3): {string.Join(", ", skipWhileIndexLessThan3)}");Chunk(size) (.NET 6+)
- بتقسم الـ
sequenceلـ “قطع” (chunks)، كل قطعة عبارة عنarrayبالحجم (size) اللي حددته (ما عدا آخر قطعة ممكن تكون أصغر). - بترجع
IEnumerable<T[]>(sequence of arrays).
var chunksOfThree = nums.Chunk(3); // [1,2,3], [4,5,6], [7,8,9], [10]
Console.WriteLine("\nChunk(3):");
foreach (var chunk in chunksOfThree)
{
Console.WriteLine($"- [{string.Join(", ", chunk)}]");
}فكرة الـ Pagination
غالبًا بنستخدم Skip() و Take() مع بعض عشان نعمل Pagination. بيكون عندك:
- الـ
pageNumber: رقم الصفحة اللي المستخدم عايزها (بيبدأ من 1). - الـ
pageSize: عدد العناصر اللي عايز تعرضها في كل صفحة (مثلًا 10).
المعادلة بتكون:
int pageNumber = 2; // Example: User wants page 2
int pageSize = 3; // Example: Show 3 items per page
var paginatedResults = nums
.Skip((pageNumber - 1) * pageSize) // Skip items of previous pages
.Take(pageSize); // Take items for the current page
// For page 2, pageSize 3: Skip((2-1)*3) = Skip(3). Take(3). Result: 4, 5, 6
Console.WriteLine($"\nPagination (Page {pageNumber}, Size {pageSize}): {string.Join(", ", paginatedResults)}");ليه مش بنستخدم Chunk() للـ Pagination؟
- الـ
Chunk()بتقسم الداتا كلها الأول لمجموعة من الـchunksفي الـmemory. ده ممكن يكون مش كويس لو الداتا جاية منdatabaseأو حجمها كبير جدًا. إنت مش عايز تحمل كل الداتا عشان تاخد صفحة واحدة بس. - الـ
Skip().Take()بتشتغلDeferred Executionبشكل أفضل. لو بتستخدمها معdatabase(باستخدامIQueryable)، الـEF Coreهيترجمها لـSQL queryبيجيب بس الداتا بتاعة الصفحة المطلوبة من الـdatabase(زي استخدامOFFSET FETCHفي SQL Server)، وده أكفأ بكتير.
12. Joins Operators (ربط الـ Sequences)
الـ Operators دي بنستخدمها عشان نربط بين اتنين sequences أو أكتر بناءً على key مشترك بينهم، زي ما بنعمل JOIN في SQL.
ملحوظة مهمة: Joins مع Local Sequences vs Databases
- كنا ممكن نشرح الـ
Joinsدي علىlocal sequences(زيList<Employee>وList<Department>في الذاكرة)، بس فايدتها الحقيقية وقوتها بتظهر أكتر لما بنستخدمها معdatabase tablesمن خلالEF Core. - لإن الـ
EF CoreبيترجمLINQ JoinsلـSQL Joinsبتتنفذ على الـdatabase serverنفسه، وده بيكون فعال جدًا.
إمتى بنستخدم الـ Join؟
بنستخدمها لما نكون محتاجين نجيب داتا مرتبطة ببعضها من أكتر من table في نفس الـ query. مثلًا، نجيب اسم الموظف واسم القسم اللي شغال فيه.
الـ Join Operators الأساسية
عندنا 2 operators أساسيين للـ Join في LINQ:
- الـ
Join(): بنستخدمها عشان نعملInner Join. سهلة ومباشرة نسبيًا. - الـ
GroupJoin(): بنستخدمها كأساس لعملLeft Outer Join(وكمان ليها استخدام تاني معlocal sequencesمش مهم أوي معdatabases).
(هنفترض إننا عندنا DbContext اسمه context فيه DbSet<Employee> اسمه Employees و DbSet<Department> اسمه Departments مربوطين بـ database).
Join() Operator (للـ Inner Join)
الـ Inner Join بيجيب بس العناصر المشتركة بين المجموعتين، يعني:
- الموظفين اللي ليهم
DepartmentIdمشnullوموجود فعلًا كـIdفيDepartments table. - الأقسام اللي فيها موظفين فعلًا.
كتابة الـ Inner Join بـ Query Syntax (الطريقة المفضلة للـ Joins)
الـ Query Syntax غالبًا بتكون أسهل في القراءة والكتابة لعمليات الـ Join.
Console.WriteLine("\n--- Inner Join (Query Syntax) ---");
// Get Employee Name and Department Name for employees who have a department
var innerJoinResultQuery =
from emp in context.Employees // Start with Employees (can start with either)
join dept in context.Departments // Join with Departments
on emp.DepartmentId equals dept.Id // The JOIN CONDITION: FK == PK
select new // Select the desired output
{
EmployeeName = emp.Name,
DepartmentName = dept.Name
};
// Execute and display
// Assuming context is an instance of your DbContext
// foreach (var item in innerJoinResultQuery)
// {
// Console.WriteLine($"Employee: {item.EmployeeName}, Department: {item.DepartmentName}");
// }
Console.WriteLine("(Query would fetch matching employees and departments)");
شرح:
- الـ
from emp in context.Employees: بنبدأ بالـtableالأول. - الـ
join dept in context.Departments: بنقوله اعملjoinمع الـtableالتاني. - الـ
on emp.DepartmentId equals dept.Id: ده شرط الربط. مهم: الطرف الشمال (emp.DepartmentId) لازم يكون الـkeyمن الـsequenceاللي جاية في جملةfromالأولى (emp)، والطرف اليمين (dept.Id) لازم يكون الـkeyمن الـsequenceاللي جاية في جملةjoin(dept). لو عكستهم ممكن يشتغل بس الأوضح تمشي بالترتيب. - الـ
select new { ... }: بنختار الحقول اللي عايزينها من الـempوالـdept.
كتابة الـ Inner Join بـ Fluent Syntax
ممكن نعمل نفس الـ Inner Join بالـ Fluent Syntax، بس الـ syntax بتاعها بيكون أطول شوية:
Console.WriteLine("\n--- Inner Join (Fluent Syntax) ---");
var innerJoinResultFluent = context.Employees // Start with the 'outer' sequence
.Join(
context.Departments, // The 'inner' sequence to join with
emp => emp.DepartmentId, // Outer Key Selector (Key from the first sequence - emp)
dept => dept.Id, // Inner Key Selector (Key from the second sequence - dept)
(emp, dept) => new // Result Selector (Combines matching emp and dept)
{
EmployeeName = emp.Name,
DepartmentName = dept.Name
}
);
// Execute and display (same result as Query Syntax)
// foreach (var item in innerJoinResultFluent)
// {
// Console.WriteLine($"Employee: {item.EmployeeName}, Department: {item.DepartmentName}");
// }
Console.WriteLine("(Query would fetch matching employees and departments)");شرح:
- الـ
context.Employees.Join(...): بنبدأ بالـsequenceالأولى ونناديJoin. - الـ
context.Departments: الـparameterالأول هو الـsequenceالتانية اللي هنعملjoinمعاها. - الـ
emp => emp.DepartmentId: الـparameterالتاني هوlambdaبتختار الـkeyمن الـsequenceالأولى (outer sequence). - الـ
dept => dept.Id: الـparameterالتالت هوlambdaبتختار الـkeyمن الـsequenceالتانية (inner sequence). - الـ
(emp, dept) => new { ... }: الـparameterالرابع هوlambdaبتحدد شكل النتيجة. بتاخد عنصر من الأولى (emp) وعنصر من التانية (dept) كـinputوبترجع الـoutputاللي إنت عايزه.
هل فيه فرق بين الطريقتين؟ لأ، الاتنين بيترجموا لنفس الـ INNER JOIN في SQL وبيدوا نفس النتيجة. استخدم اللي تريحك، بس الـ Query Syntax غالبًا أسهل للـ Join.
ملحوظة عن Overload الـ IEqualityComparer
- فيه
overloadتاني للـmethodبتاعتJoinبياخدparameterزيادة من نوعIEqualityComparer. - اتكلمنا عن الـ
interfaceده زمان مع الـSet Operators(زيDistinct,Union…). فايدته إنه بيخليك تتحكم إزاي بيتم مقارنة الـobjects. - امتى ممكن نحتاجه نظريًا؟
- لما بنعمل
joinبينlocal sequences(مشdatabase tables) والـkeysاللي بنقارنها عبارة عنcomplex objects(زيclassاسمهAddress) ومشprimitive types(زيintأوstring). في الحالة دي، المقارنة العادية هتقارنreferences، لكن إحنا ممكن نكون عايزين نقارن الـvaluesاللي جوا الـobjects. الـIEqualityComparerبيسمح بده. - أو في حالة الـ
composite primary keysالمعقدة جدًا (سيناريو نادر).
- لما بنعمل
- هل بنحتاجه مع الـ Database؟
- لأ، غالبًا مش بنحتاجه خالص. ليه؟ لأن الـ
Primary KeysوالـForeign Keysفي الـdatabaseبتكون غالبًاprimitive types(int,string,Guid…) والـEntity Frameworkبيعرف يقارنهم ببعض عادي جدًا من غيرIEqualityComparer. - الـ
overloadده غالبًاnot translatedلـSQLبشكل صحيح لو حاولت تستخدمه معdatabase query.
- لأ، غالبًا مش بنحتاجه خالص. ليه؟ لأن الـ
- الخلاصة: انسى الـ
overloadده دلوقتي، مش هنستخدمه مع الـdatabase.
GroupJoin() Operator (واستخدامه لعمل Left Join)
نيجي بقى للـ operator التاني، اللي هو GroupJoin. ده ليه استخدام معين وممكن يكون مربك شوية.
استخدام الـ GroupJoin الأساسي (Join ثم Grouping by Object)
- الاستخدام الأساسي أو الفكرة الأصلية ورا
GroupJoinهي إنك تعملjoinبينtwo sequences، وبعدين تعملgroupingللنتائج. - التركة فين؟ الـ
Groupingاللي بيعملهGroupJoinبيكونbased on the entire objectمن الـouter sequence، مشbased on a specific column. - مثال: لو عملت
GroupJoinبينDepartmentsوEmployees، ممكن النتيجة تكون كأنك بتقول “لكلDepartmentobject، هاتلي قايمة بالـEmployeesبتوعه”.
تجربة الـ GroupJoin مع الـ Database (مشكلة الترجمة)
تعالوا نشوف شكل الـ fluent syntax بتاع GroupJoin:
// Example starting with Departments to group by Department object
var groupJoinResult = context.Departments
.GroupJoin(
context.Employees, // Inner sequence (Employees)
d => d.Id, // Outer Key Selector (PK of Department)
e => e.DepartmentId, // Inner Key Selector (FK of Employee)
(d, emps) => new // Result Selector (takes department object, and IEnumerable of matching employees)
{
Department = d, // The Department object itself acts as the key for grouping
Employees = emps // The collection of employees for that department
}
);
// Trying to iterate (this will likely throw an exception when hitting the database)
// foreach (var item in groupJoinResult)
// {
// Console.WriteLine($"Department: {item.Department.Name}");
// foreach (var emp in item.Employees)
// {
// Console.WriteLine($" - Emp: {emp.Name}");
// }
// }شرح الكود:
- الـ
context.Departments.GroupJoin(...): بنبدأ بالـDbSetاللي عايزين نعملgroupingعلى أساس الـobjectsبتاعته (هناDepartments). - الـ
context.Employees,d => d.Id,e => e.DepartmentId: دول زي الـJoinالعادي (الـinner sequenceوالـkeys). - الـ
(d, emps) => new { ... }: الـresult selectorهنا مختلف.- الـ
parameterالأول (d) بيمثل الـobjectمن الـouter sequence(الـDepartmentobject). - الـ
parameterالتاني (emps) مش بيمثلemployeeواحد، لأ ده بيمثلIEnumerable<Employee>، يعني قايمة بكل الـemployeesاللي الـDepartmentIdبتاعهم بيساوي الـIdبتاع الـDepartmentالحالي (d). - رجعنا
anonymous typeفيه الـDepartmentobject نفسه ومجموعة الـEmployeesبتاعته.
- الـ
الفرق بين Cs IQueryable و Cs IEnumerable؟
من الآخر هنا الـ GroupJoin تعتبر Cs Extension Methods لـ Queryable أو Enumerable فاللي بنفذهم من Queryable دي بينفذ الأمر في الـ Database ويجيبلي الداتا خلصانة هنا
انما الـ Cs Enumerable هيجيبلي الداتا كلها عندك في الـ Memory ويبدأ ينفذ الـ Query هنا
إيه المشكلة لما نشغل الكود ده على الـ Database؟
- لما الـ
Entity Framework Coreيحاول يترجم الـqueryده لـSQL، هيفشل وهيضربexceptionغالبًا بتقولcould not be translated. - ليه؟ لأن
SQLمفيهوش حاجة اسمها نعملgrouping based on a whole recordأوobject. الـGROUP BYفيSQLلازم يكونbased on specific columns(زيDepartmentNameأوDepartmentId). - فـ الاستخدام الأساسي ده للـ
GroupJoinمينفعش نطبقه مباشرة على الـdatabase.
الاستخدام ده ينفع فين؟
- ينفع لو شغالين على
local sequences(زيList<Department>وList<Employee>في الـ memory). ساعتها الـLINQ to Objectsهيقدر يعملgrouping based on objectعادي.
استخدام الـ GroupJoin لعمل Left Join (الطريقة الرسمية)
الـ GroupJoin لوحده ليه استخدام أساسي (يعمل join وبعدين grouping based on the outer object)، بس زي ما قلنا، الاستخدام ده مش بيترجم كويس لـ SQL.
الفايدة الحقيقية للـ GroupJoin مع EF Core هي إنه بيكون خطوة أساسية عشان نعمل Left Outer Join.
الـ Left Join بيجيب كل عناصر الـ sequence الشمال (Left)، والعناصر المطابقة ليها من الـ sequence اليمين (Right). لو عنصر في الشمال ملوش مقابل في اليمين، بيجيبه برضه بس بيحط null (أو default) مكان قيم اليمين.
مفيش method مباشرة اسمها .LeftJoin() في LINQ القياسي نقدر نستخدمها مع EF Core. بنعملها باستخدام GroupJoin مع SelectMany و DefaultIfEmpty.
عمل Left Join بـ Query Syntax (باستخدام into) - الأسهل للقراءة
الطريقة دي هي الأوضح لعمل Left Join:
Console.WriteLine("\n--- Left Join (Query Syntax using 'into') ---");
// Get ALL Departments, and their corresponding Employees (or null if none)
var leftJoinQuery =
from dept in context.Departments // Start with the LEFT table (Departments)
join emp in context.Employees // Join with the RIGHT table (Employees)
on dept.Id equals emp.DepartmentId // Join condition
into empGroup // *** Use 'into' to put matching employees into a temporary group ***
from employeeInGroup in empGroup.DefaultIfEmpty() // *** Iterate the group, use DefaultIfEmpty() for departments with no employees ***
select new
{
DepartmentName = dept.Name,
// EmployeeName will be null if employeeInGroup is null (due to DefaultIfEmpty)
EmployeeName = employeeInGroup == null ? "!!No Employees!!" : employeeInGroup.Name
};
// Execute and display
// foreach (var item in leftJoinQuery)
// {
// Console.WriteLine($"Department: {item.DepartmentName}, Employee: {item.EmployeeName}");
// }
Console.WriteLine("(Query would fetch all departments and their employees, showing 'No Employees' for empty depts)");
شرح الـ Magic:
- الـ
from dept ... join emp ... on ...: زي الـInner Joinعادي. - الـ
into empGroup: دي أهم حتة. بدل ما الـjoinيرجع (dept, emp) لكل ماتش، بنقوله حط كل الـempاللي بيقابلوا الـdeptده في مجموعة مؤقتة اسمهاempGroup. الـempGroupده هيكونIEnumerable<Employee>. - الـ
from employeeInGroup in empGroup.DefaultIfEmpty(): بنعملloopتانية على الـempGroupده.- الـ
DefaultIfEmpty(): دي بتعمل إيه؟ لو الـempGroupكان فاضي (يعني القسم ده مكنش ليه موظفين في الـjoin)، الـmethodدي بترجعsequenceفيها عنصر واحد بس قيمتهnull(أو الـdefaultبتاعEmployee). لو الـempGroupمش فاضي، بترجعه زي ما هو. - فلما نعمل
from employeeInGroup in ...، لو القسم ليه موظفين، الـloopهتلف على كل موظف وemployeeInGroupهيبقى هو الموظف ده. لو القسم مفيهوش موظفين، الـloopهتلف مرة واحدة بس وemployeeInGroupهيبقى قيمتهnull.
- الـ
- الـ
select new { ... }: بنختار النتيجة. الـdept.Nameجاي من الـfromالأولى. الـEmployeeNameبناخده منemployeeInGroup، بس لازم نعملcheckإنه مشnullالأول.
عمل Left Join بـ Fluent Syntax (باستخدام GroupJoin().SelectMany())
الطريقة دي بتادي نفس الغرض، بس الـ syntax بتاعها أعقد شوية:
Console.WriteLine("\n--- Left Join (Fluent Syntax using GroupJoin + SelectMany) ---");
var leftJoinFluent = context.Departments // Start with LEFT table
.GroupJoin( // Step 1: GroupJoin
context.Employees, // Right table
dept => dept.Id, // Outer Key (Left)
emp => emp.DepartmentId, // Inner Key (Right)
(dept, empGroup) => new { Department = dept, Employees = empGroup } // Project to {Dept, GroupOfEmps}
)
.SelectMany( // Step 2: Flatten using SelectMany and handle empty groups
deptAndGroup => deptAndGroup.Employees.DefaultIfEmpty(), // Apply DefaultIfEmpty to the employee group
(deptAndGroup, employeeInGroup) => new // Result selector for SelectMany
{
DepartmentName = deptAndGroup.Department.Name,
EmployeeName = employeeInGroup == null ? "!!No Employees!!" : employeeInGroup.Name
}
);
// Execute and display (same result as Query Syntax)
// foreach (var item in leftJoinFluent)
// {
// Console.WriteLine($"Department: {item.DepartmentName}, Employee: {item.EmployeeName}");
// }
Console.WriteLine("(Query would fetch all departments and their employees, showing 'No Employees' for empty depts)");شرح:
- بنبدأ بـ
GroupJoinزي ما عملناه قبل كده، بيرجعsequenceمن{Department, EmployeesGroup}. - بنستخدم
SelectManyعشان نعملflatten. - الجزء الأول من
SelectMany(deptAndGroup => deptAndGroup.Employees.DefaultIfEmpty()) بياخد كلgroupمن الموظفين، ولو فاضية بيرجعsequenceفيهاnull. - الجزء التاني من
SelectMany((deptAndGroup, employeeInGroup) => new { ... }) بياخد الـobjectالأصلي (deptAndGroup) وبياخد كل عنصر من نتيجة الـDefaultIfEmpty(اللي هو ياEmployeeياnull)، وبيعملselectللنتيجة النهائية.
الخلاصة للـ Left Join: الـ Query Syntax باستخدام into غالبًا بتكون أسهل وأوضح بكتير من الـ Fluent Syntax باستخدام GroupJoin().SelectMany().
Cross Join (Cartesian Product)
الـ Cross Join بيجيب كل الاحتمالات الممكنة بين مجموعتين، يعني كل عنصر من الأولى مع كل عنصر من التانية. (استخداماته قليلة في الـ Business).
عمل Cross Join بـ Query Syntax (اتنين from)
الطريقة بسيطة جدًا: مجرد اتنين from ورا بعض من غير join أو where بينهم.
Console.WriteLine("\n--- Cross Join (Query Syntax) ---");
// Get every possible combination of Employee and Department
var crossJoinQuery =
from emp in context.Employees // First sequence
from dept in context.Departments // Second sequence (NO 'join' or 'on')
select new
{
EmployeeName = emp.Name,
DepartmentName = dept.Name
};
// Execute and display
// foreach (var item in crossJoinQuery)
// {
// Console.WriteLine($"Employee: {item.EmployeeName}, Department: {item.DepartmentName}");
// }
Console.WriteLine("(Query would fetch all combinations of employees and departments)");
عمل Cross Join بـ Fluent Syntax (SelectMany)
الـ Fluent Syntax المكافئ لاتنين from هو غالبًا SelectMany.
Console.WriteLine("\n--- Cross Join (Fluent Syntax) ---");
var crossJoinFluent = context.Employees // Start with first sequence
.SelectMany(
emp => context.Departments, // For each employee, the collection to cross with is ALL departments
(emp, dept) => new // Result selector combining employee and department
{
EmployeeName = emp.Name,
DepartmentName = dept.Name
}
);
// Alternative SelectMany structure (achieves the same)
var crossJoinFluentAlt = context.Employees
.SelectMany(emp => context.Departments.Select(dept => new { emp, dept })) // Create pairs first
.Select(pair => new { EmployeeName = pair.emp.Name, DepartmentName = pair.dept.Name }); // Select final
// Execute and display (same result as Query Syntax)
// foreach (var item in crossJoinFluent)
// {
// Console.WriteLine($"Employee: {item.EmployeeName}, Department: {item.DepartmentName}");
// }
Console.WriteLine("(Query would fetch all combinations of employees and departments)");
الخلاصة للـ Cross Join: الـ Query Syntax (اتنين from) بتكون أوضح وأسهل في القراءة.
نقطة أخيرة: امتى أستخدم Join صريحة بدل .Include() للـ Many؟ (عشان الـ Performance)
نرجع تاني لنقطة الـ Eager Loading (.Include()) للـ Navigational Property Many (زي حالة Order و OrderItems اللي قلنا لازم نعملها Eager Loading).
- لما بنستخدم
.Include()معNavigational Property Many، الـEntity Framework Coreبيترجمها لـSQL. أحيانًا (مش دايمًا)، الـSQLالناتج ده ممكن ميكونش الأمثل من ناحية الـperformance. ممكن يحتوي علىjoinومعاهاsubqueryأو طريقة الـjoinنفسها متكونش الأفضل. - الحل (في حالات نادرة جدًا لو الـ
Performancecriticalأوي):- بدل ما تستخدم
.Include(o => o.OrderItems)، ممكن تكتب الـJoinبينOrdersوOrderItemsبإيدك (باستخدامJoinoperator زي ما شرحنا في الأول). - ليه؟ ده ممكن يدي الـ
database query optimizerحرية أكبر إنه يختار طريقة الـjoinالأفضل، أو يتجنب الـsubqueryاللي ممكن.Include()تعمله. - تحذير: دي تعتبر
advanced optimization. متعملهاش غير لو أنت متأكد إن.Include()عامل مشكلةperformanceحقيقية وقست الفرق بنفسك. في معظم الحالات،.Include()بيكون كافي ومقروء أكتر.
- بدل ما تستخدم
معلومات إضافية (More Information)
let Keyword
- بنستخدم
letجوه الـQuery Syntaxعشان نعرفvariableمؤقت نقدر نخزن فيه نتيجة عملية حسابية أوsub-expressionعشان نستخدمها أكتر من مرة في نفس الـqueryأو عشان نخليه مقروء أكتر. - الـ
variableده بيكون متاح في باقي أجزاء الـqueryاللي بتيجي بعد الـlet(زيwhereأوorderbyأوselect).
مثال: هات أسماء الموظفين اللي اسمهم (بعد تحويله لـ uppercase) بيبدأ بحرف ‘A’، ورتبهم بالاسم الـ uppercase ده.
// Using 'let' in Query Syntax
var queryWithLet =
from e in employees
let upperCaseName = e.Name.ToUpper() // Define temporary variable
where upperCaseName.StartsWith("A") // Use the variable
orderby upperCaseName // Use the variable again
select upperCaseName; // Select the variable
Console.WriteLine("\nQuery with 'let':");
Console.WriteLine(string.Join(", ", queryWithLet)); // AHMED, ALIinto Keyword
اتكلمنا عنها فوق مع group by و join. استخدامها الأساسي هو إنها بتكمل الـ query بعد مرحلة معينة (زي group أو join)، وبتعرف variable جديد للمرحلة الجديدة دي. كأنها بتعمل restart للـ query بس بالنتيجة المؤقتة اللي وصلت لها.
- مع
group by ... into groupVariable: بنكمل شغل على الـgroupsاللي اتكونت. - مع
join ... into groupVariable: بنكمل شغل على الـgroupsاللي اتكونت ضمنيًا عشان نعمل غالبًاLeft Join. - مع
select ... into variable: ممكن تستخدمها عشان تاخد نتيجةselectوتكمل عليهاqueryتاني، بس دي استخدامها أقل شيوعًا لإننا ممكن نعملchainingبالـFluent Syntaxأسهل.
الفرق بين let و into:
- الـ
let: بتعرفvariableجوه نفس الـquery scopeعشان تستخدمه في الأجزاء اللي باقية من الـqueryده. بتكمل الـqueryبنفس الـrange variableالأصلي + الـvariableالجديد. - الـ
into: بتنهي الـquery scopeالحالي وتبدأscopeجديد بالنتيجة اللي وصلت لها. الـrange variablesاللي كانت قبلinto(ما عدا اللي في جملةintoنفسها) مبتبقاش متاحة بعدinto. كأنها بتعملrestartللـquery.
استخدام Regex مع LINQ
ممكن نستخدم Regular Expressions (اللي اتكلمنا عنها قبل كده) جوه LINQ queries (خصوصًا مع Select أو Where) عشان نعمل عمليات معقدة على الـ strings.
مثال: شيل كل الحروف المتحركة (vowels) من أسماء الموظفين.
using System.Text.RegularExpressions; // Need this namespace
Console.WriteLine("\nUsing Regex with LINQ:");
List<string> namesList = employees.Select(e => e.Name).ToList(); // Get names
// Select names without vowels using Regex.Replace
var namesWithoutVowels = namesList.Select(name => Regex.Replace(name, "[aeiouAEIOU]", ""));
Console.WriteLine("Names without vowels:");
Console.WriteLine(string.Join(", ", namesWithoutVowels)); // Ahmd, Mn, Al, Sr
// Example: Filter names that become longer than 3 chars AFTER removing vowels using 'let'
var longNamesAfterNoVowelsLet =
from name in namesList
let noVowelName = Regex.Replace(name, "[aeiouAEIOU]", "")
where noVowelName.Length > 3 // Check length of the result
select noVowelName;
Console.WriteLine("\nLong names (>3) after removing vowels (let):");
Console.WriteLine(string.Join(", ", longNamesAfterNoVowelsLet)); // Ahmd
// Example: Filter names that become longer than 3 chars AFTER removing vowels using 'into'
var longNamesAfterNoVowelsInto =
from name in namesList
select Regex.Replace(name, "[aeiouAEIOU]", "") // Select the modified name
into noVowelName // Put result into new variable, restarting query scope
where noVowelName.Length > 3 // Filter based on the new variable
select noVowelName;
Console.WriteLine("\nLong names (>3) after removing vowels (into):");
Console.WriteLine(string.Join(", ", longNamesAfterNoVowelsInto)); // Ahmd
// Equivalent using Fluent Syntax (often simpler here)
var longNamesAfterNoVowelsFluent = namesList
.Select(name => Regex.Replace(name, "[aeiouAEIOU]", "")) // Transform
.Where(noVowelName => noVowelName.Length > 3); // Filter
Console.WriteLine("\nLong names (>3) after removing vowels (Fluent):");
Console.WriteLine(string.Join(", ", longNamesAfterNoVowelsFluent)); // Ahmdفي الحالة دي، استخدام Fluent Syntax أو Query Syntax مع let بيكون غالبًا أسهل من استخدام into.
النهاية (End)
- راجع الـ documentation الرسمي.
- حل تمارين (زي دي على Northwind database).
- ممكن تبص على ملخصات زي LINQ Summary.pdf (لو متاح).
- ابحث عن تمارين تانية زي w3resource LINQ exercises.