قبل ما نبدأ نتكلم عن الـ 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:
    1. العنصر نفسه (product).
    2. الـ 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” بيعتمد على مصدر الداتا:

  1. Local Sequence:

    • هي أي Sequence موجودة في الذاكرة (in-memory) بتاعة الأبلكيشن بتاعك، زي List أو Array عملتهم بنفسك.
    • لما بتستخدم LINQ معاها، العملية دي بنسميها LINQ to Objects (L2O).
    • فيه كمان LINQ to XML (L2XML) لو بتعمل query على فايل XML files موجود عندك.
    • كل العمليات (الفلترة، الترتيب، إلخ) بتحصل جوه الـ RAM بتاعة الجهاز اللي شغال عليه الأبلكيشن. مفيش اتصال بمصدر خارجي.
    • حتى فايلات الـ XML بنعتبرها Local في السياق ده.
  2. 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 بتشتغل بالطريقة المؤجلة دي؟ إيه فوايدها؟

  1. الـ Performance (الأداء):
    • الـ Query مش بيتنفذ إلا لما تحتاجه فعلًا. لو عرفت query ومعملتش عليه loop أو محولتهوش لـ List، مش هيستهلك أي وقت تنفيذ تقريبًا.
    • لو عندك query بيرجع داتا كتير، وانت محتاج بس أول عنصر (FirstOrDefault()) أو أول كام عنصر (Take(5)), الـ query هيتنفذ بس لحد ما يجيب المطلوب ومش هيكمل باقي الداتا.
  2. الـ Memory Efficiency (كفاءة الذاكرة):
    • النتائج مش بتتخزن في الـ memory كلها مرة واحدة (إلا لو طلبت ده بـ ToList() مثلًا). ده كويس جدًا لو بتتعامل مع حجم داتا كبير جدًا ممكن ميكفيش في الـ memory.
  3. الـ Composability (التركيب):
    • تقدر تبني query معقد على مراحل. ممكن تعرف query أساسي، وبعدين تضيف عليه filter تاني (.Where(...)) أو ترتيب (.OrderBy(...)) حسب الحاجة، وكل ده هيتجمع وهيتركب مع بعض ويتنفذ مرة واحدة في الآخر.
  4. الـ Always Fresh Data (دايمًا أحدث داتا):
    • زي ما شفنا في المثال، لإن الـ query بيتنفذ لما تطلب النتيجة، فهو دايمًا بيشتغل على آخر نسخة من الداتا اللي في الـ source list. لو عدلت في الـ list بعد ما عرفت الـ query وقبل ما تنفذه، التعديلات دي هتنعكس في النتيجة.
  5. الـ 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()).
  • لما بتستخدم واحد من الـ 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 أساسية بتشتغل بالطريقة دي:

  1. الـ Element Operators اللي بتجيب عنصر واحد (زي First, Last, Single, ElementAt ومشتقاتهم).
  2. الـAggregate Operator اللي بتعمل عمليات حسابية أو تجميعية على كل الـ sequence عشان ترجع قيمة واحدة (زي Count, Sum, Average, Min, Max, Aggregate, Any, All).
  3. الـ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()بيرجع العنصر الوحيد.ExceptionExceptionException
SingleOrDefault()بيرجع العنصر الوحيد، أو القيمة الافتراضية لو فاضية.بيرجع DefaultExceptionبيرجع Default
ElementAt(index)بيرجع العنصر عند فهرس (index) معين (بيبدأ من 0).Exception (لو index غلط)N/AException (لو 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:
    1. الـ keySelector: ازاي تجيب الـ Key بتاع الـ Dictionary من كل عنصر في الـ sequence. الـ Key لازم يكون unique.
    2. الـ 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):
    1. الـ Equals(object obj): تعرف فيها إمتى تعتبر اتنين objects من الـ class ده متساويين (مثلًا لو ليهم نفس الـ Id).
    2. الـ 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 عشان تشوف هل هما متساويتين.
  • بتكون متساوية لو:
    1. ليهم نفس عدد العناصر.
    2. كل عنصر في الـ 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 بيكون ليها حاجتين أساسيتين:
    1. الـ Key: القيمة اللي تم التجميع بيها (مثلًا اسم القسم “HR”).
    2. الـ 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:

  1. الـ Join(): بنستخدمها عشان نعمل Inner Join. سهلة ومباشرة نسبيًا.
  2. الـ 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)");
 

شرح:

  1. الـ from emp in context.Employees: بنبدأ بالـ table الأول.
  2. الـ join dept in context.Departments: بنقوله اعمل join مع الـ table التاني.
  3. الـ on emp.DepartmentId equals dept.Id: ده شرط الربط. مهم: الطرف الشمال (emp.DepartmentId) لازم يكون الـ key من الـ sequence اللي جاية في جملة from الأولى (emp)، والطرف اليمين (dept.Id) لازم يكون الـ key من الـ sequence اللي جاية في جملة join (dept). لو عكستهم ممكن يشتغل بس الأوضح تمشي بالترتيب.
  4. الـ 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)");

شرح:

  1. الـ context.Employees.Join(...): بنبدأ بالـ sequence الأولى وننادي Join.
  2. الـ context.Departments: الـ parameter الأول هو الـ sequence التانية اللي هنعمل join معاها.
  3. الـ emp => emp.DepartmentId: الـ parameter التاني هو lambda بتختار الـ key من الـ sequence الأولى (outer sequence).
  4. الـ dept => dept.Id: الـ parameter التالت هو lambda بتختار الـ key من الـ sequence التانية (inner sequence).
  5. الـ (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}");
//     }
// }

شرح الكود:

  1. الـ context.Departments.GroupJoin(...): بنبدأ بالـ DbSet اللي عايزين نعمل grouping على أساس الـ objects بتاعته (هنا Departments).
  2. الـ context.Employees, d => d.Id, e => e.DepartmentId: دول زي الـ Join العادي (الـ inner sequence والـ keys).
  3. الـ (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:

  1. الـ from dept ... join emp ... on ...: زي الـ Inner Join عادي.
  2. الـ into empGroup: دي أهم حتة. بدل ما الـ join يرجع (dept, emp) لكل ماتش، بنقوله حط كل الـ emp اللي بيقابلوا الـ dept ده في مجموعة مؤقتة اسمها empGroup. الـ empGroup ده هيكون IEnumerable<Employee>.
  3. الـ from employeeInGroup in empGroup.DefaultIfEmpty(): بنعمل loop تانية على الـ empGroup ده.
    • الـ DefaultIfEmpty(): دي بتعمل إيه؟ لو الـ empGroup كان فاضي (يعني القسم ده مكنش ليه موظفين في الـ join)، الـ method دي بترجع sequence فيها عنصر واحد بس قيمته null (أو الـ default بتاع Employee). لو الـ empGroup مش فاضي، بترجعه زي ما هو.
    • فلما نعمل from employeeInGroup in ...، لو القسم ليه موظفين، الـ loop هتلف على كل موظف و employeeInGroup هيبقى هو الموظف ده. لو القسم مفيهوش موظفين، الـ loop هتلف مرة واحدة بس و employeeInGroup هيبقى قيمته null.
  4. الـ 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)");

شرح:

  1. بنبدأ بـ GroupJoin زي ما عملناه قبل كده، بيرجع sequence من {Department, EmployeesGroup}.
  2. بنستخدم SelectMany عشان نعمل flatten.
  3. الجزء الأول من SelectMany (deptAndGroup => deptAndGroup.Employees.DefaultIfEmpty()) بياخد كل group من الموظفين، ولو فاضية بيرجع sequence فيها null.
  4. الجزء التاني من 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)