قبل ما نبدأ نتكلم عن الـ 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
). يعني كأننا خدنا الـSELECT
statement وحطيناها جوه 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
من الـEnumerable
class وبنمرر له الـ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
(شرط) عشان يلاقي أول/آخر/وحيد عنصر بيحقق الشرط ده. - الـ
OrDefault
versions (زي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
/Length
property عشان تجيب العدد بسرعة. - لو الـ
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 now
ToArray()
- بتحول الـ
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()}"); // True
7. 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()
- بتجيب اتحاد مجموعتين (
sequence
1 +sequence
2)، مع إزالة العناصر المكررة بين المجموعتين أو جوه كل مجموعة.
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
، ممكن النتيجة تكون كأنك بتقول “لكلDepartment
object
، هاتلي قايمة بالـ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
(الـDepartment
object). - الـ
parameter
التاني (emps
) مش بيمثلemployee
واحد، لأ ده بيمثلIEnumerable<Employee>
، يعني قايمة بكل الـemployees
اللي الـDepartmentId
بتاعهم بيساوي الـId
بتاع الـDepartment
الحالي (d
). - رجعنا
anonymous type
فيه الـDepartment
object نفسه ومجموعة الـ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
نفسها متكونش الأفضل. - الحل (في حالات نادرة جدًا لو الـ
Performance
critical
أوي):- بدل ما تستخدم
.Include(o => o.OrderItems)
، ممكن تكتب الـJoin
بينOrders
وOrderItems
بإيدك (باستخدامJoin
operator زي ما شرحنا في الأول). - ليه؟ ده ممكن يدي الـ
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, ALI
into
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.