آموزش LINQ در C#: ساده‌سازی کوئری‌ها و کار با داده

فهرست مطالب

آموزش جامع LINQ در C#: ساده‌سازی بی‌سابقه کوئری‌ها و کار با داده‌ها

در دنیای برنامه‌نویسی مدرن، کار با داده‌ها در اشکال و منابع مختلف اجتناب‌ناپذیر است. از پایگاه‌های داده رابطه‌ای و غیررابطه‌ای گرفته تا فایل‌های XML، اسناد JSON، و حتی مجموعه‌های ساده در حافظه، توسعه‌دهندگان به طور مداوم با نیاز به فیلتر کردن، مرتب‌سازی، گروه‌بندی، و دستکاری اطلاعات مواجه هستند. پیش از ظهور LINQ (Language Integrated Query) در چارچوب .NET، هر منبع داده رویکرد کوئری‌نویسی خاص خود را داشت. برای مثال، برای کوئری گرفتن از یک پایگاه داده از SQL، برای XML از XPath/XQuery و برای مجموعه‌های حافظه از حلقه‌ها و دستورات شرطی استفاده می‌شد. این پراکندگی نه تنها منجر به پیچیدگی کد و دشواری نگهداری می‌شد، بلکه خطاهای تایپی و منطقی را نیز افزایش می‌داد، زیرا کامپایلر اغلب قادر به اعتبارسنجی کوئری‌ها تا زمان اجرا نبود.

LINQ به عنوان یک نوآوری انقلابی در C# 3.0 معرفی شد تا این مشکلات را حل کند. هدف اصلی LINQ ارائه یک مدل برنامه‌نویسی یکپارچه و یکپارچه برای کوئری گرفتن از انواع منابع داده است. با LINQ، می‌توانید از طریق یک سینتکس قدرتمند و شهودی، که به طور طبیعی با زبان C# ادغام شده است، داده‌ها را کوئری کنید. این یکپارچگی به معنای بهره‌مندی از امکانات IDE مانند IntelliSense، بررسی خطاهای زمان کامپایل (Type Safety) و قابلیت Debugging قوی‌تر است. به جای یادگیری چندین زبان کوئری مختلف، توسعه‌دهندگان می‌توانند با یک مجموعه مهارت واحد، به تمام داده‌های خود دسترسی پیدا کرده و آن‌ها را دستکاری کنند. این مقاله جامع به بررسی عمیق LINQ در C# می‌پردازد، از مفاهیم بنیادی آن گرفته تا پیاده‌سازی‌های پیشرفته و بهترین شیوه‌ها، تا شما را قادر سازد کوئری‌های خود را به شکلی کارآمدتر، خواناتر و قابل نگهداری‌تر بنویسید.

1. مقدمه‌ای بر LINQ: مفهوم، ضرورت و مزایا

LINQ، مخفف Language Integrated Query، یک ویژگی قدرتمند در زبان C# است که به شما امکان می‌دهد با استفاده از یک سینتکس یکپارچه، از انواع مختلف منابع داده کوئری بگیرید. این منابع می‌توانند شامل مجموعه‌های داده در حافظه (مانند `List` یا آرایه‌ها)، پایگاه‌های داده رابطه‌ای (از طریق LINQ to SQL یا Entity Framework Core)، اسناد XML (با LINQ to XML) و حتی سرویس‌های داده (مانند OData) باشند. ایده اصلی LINQ این است که زبان C# را به قدری انعطاف‌پذیر کند که بتواند به طور بومی دستورات کوئری را در خود جای دهد، دقیقاً همانند کدهای دستوری و شی‌گرا.

1.1. چرا به LINQ نیاز داریم؟ مشکلات پیش از LINQ

پیش از معرفی LINQ، توسعه‌دهندگان برای تعامل با داده‌ها در منابع مختلف با چالش‌های متعددی روبرو بودند:

  • تنوع زبان‌های کوئری: برای هر منبع داده، باید زبان کوئری خاص خود را یاد می‌گرفتید و استفاده می‌کردید (مثلاً SQL برای پایگاه داده، XPath برای XML، حلقه‌های `foreach` برای مجموعه‌های حافظه). این امر منجر به افزایش بار شناختی و زمان یادگیری می‌شد.
  • عدم Type Safety: کوئری‌های رشته‌ای (مانند SQLهای دینامیک) در زمان کامپایل بررسی نمی‌شدند. خطاهای تایپی یا املایی در نام ستون‌ها و جداول تنها در زمان اجرا کشف می‌شدند که Debugging را دشوار می‌کرد.
  • کاهش خوانایی و نگهداری: ترکیب کدهای C# با رشته‌های SQL یا XPath، خوانایی کد را کاهش داده و فرآیند نگهداری و Refactoring را پیچیده می‌کرد.
  • پایبندی به تکنولوژی خاص: انتقال کد از یک پایگاه داده به پایگاه داده دیگر یا از یک تکنولوژی XML به دیگری نیازمند بازنویسی قابل توجه بود.
  • تفاوت در مدل‌های شی‌گرایی: نگاشت نتایج کوئری‌ها از ساختارهای جدولی یا سلسله‌مراتبی به مدل‌های شی‌گرای C# اغلب دستی و مستعد خطا بود.

1.2. مزایای کلیدی LINQ

LINQ این مشکلات را با ارائه مزایای زیر حل می‌کند:

  • سینتکس یکپارچه: یک سینتکس واحد برای کوئری گرفتن از تمام منابع داده. این باعث کاهش زمان یادگیری و افزایش بهره‌وری می‌شود.
  • Type Safety: کوئری‌ها در زمان کامپایل بررسی می‌شوند. IntelliSense در Visual Studio به شما کمک می‌کند تا کوئری‌های صحیح بنویسید و خطاهای تایپی را پیش از اجرا تشخیص دهید.
  • خوانایی بالاتر: کوئری‌ها به صورت مستقیم در کد C# نوشته می‌شوند و با قابلیت‌های زبان مانند عبارات Lambda و انواع ناشناس (Anonymous Types) ترکیب می‌شوند که کد را خواناتر می‌کند.
  • کاهش کد boilerplate: LINQ بسیاری از عملیات رایج داده را به صورت خلاصه و اعلانی (Declarative) امکان‌پذیر می‌سازد و نیاز به نوشتن حلقه‌های طولانی و شرط‌های پیچیده را از بین می‌برد.
  • قابلیت Debugging آسان: می‌توانید خط به خط کوئری‌های LINQ خود را Debug کنید، درست مانند هر کد C# دیگری.
  • پشتیبانی از IntelliSense: ابزارهای توسعه مانند Visual Studio به طور کامل از LINQ پشتیبانی می‌کنند و پیشنهادهای کد و تکمیل خودکار را ارائه می‌دهند.
  • تنوع Providerها: LINQ با انواع مختلفی از داده‌ها کار می‌کند که هر کدام از طریق یک “Provider” خاص پیاده‌سازی می‌شوند (LINQ to Objects, LINQ to XML, LINQ to SQL, LINQ to Entities).

2. ساختار و سینتکس LINQ: Query Syntax در برابر Method Syntax

LINQ به دو روش اصلی برای نوشتن کوئری‌ها اجازه می‌دهد: Query Syntax (سینتکس کوئری) و Method Syntax (سینتکس متد). هر دو سینتکس معادل یکدیگر هستند و می‌توانند برای انجام یک کار مشابه استفاده شوند، اما هر کدام دارای ویژگی‌ها و مزایای خاص خود هستند.

2.1. Query Syntax (سینتکس کوئری)

Query Syntax شبیه به سینتکس SQL است و برای توسعه‌دهندگانی که با SQL آشنا هستند، ممکن است آشناتر به نظر برسد. این سینتکس با کلمه کلیدی `from` آغاز می‌شود و به صورت پشت سر هم از کلمات کلیدی LINQ مانند `where`, `select`, `group by`, `orderby` استفاده می‌کند. Query Syntax در زمان کامپایل به Method Syntax معادل تبدیل می‌شود.


using System;
using System.Collections.Generic;
using System.Linq;

public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    public double Grade { get; set; }
}

public class LinqExamples
{
    public static void Main(string[] args)
    {
        List<Student> students = new List<Student>
        {
            new Student { Id = 1, Name = "علی", Age = 20, Grade = 85.5 },
            new Student { Id = 2, Name = "مریم", Age = 22, Grade = 92.0 },
            new Student { Id = 3, Name = "حسین", Age = 21, Grade = 78.0 },
            new Student { Id = 4, Name = "فاطمه", Age = 20, Grade = 95.0 },
            new Student { Id = 5, Name = "رضا", Age = 23, Grade = 88.0 },
            new Student { Id = 6, Name = "زهرا", Age = 22, Grade = 70.0 }
        };

        // مثال Query Syntax: انتخاب دانش‌آموزان بالای 20 سال با نمره بالای 80
        var selectedStudents = from student in students
                               where student.Age > 20 && student.Grade > 80
                               orderby student.Name ascending
                               select new { student.Name, student.Grade };

        Console.WriteLine("دانش‌آموزان بالای 20 سال با نمره بالای 80 (Query Syntax):");
        foreach (var s in selectedStudents)
        {
            Console.WriteLine($"- نام: {s.Name}, نمره: {s.Grade}");
        }
    }
}

2.2. Method Syntax (سینتکس متد)

Method Syntax از متدهای توسعه‌یافته (Extension Methods) که بر روی انواع `IEnumerable` و `IQueryable` تعریف شده‌اند، استفاده می‌کند. این متدها (مانند `Where`, `Select`, `OrderBy`) به صورت زنجیره‌ای فراخوانی می‌شوند. Method Syntax به دلیل انعطاف‌پذیری بیشتر و قابلیت ترکیب با عبارات Lambda، اغلب برای کوئری‌های پیچیده‌تر یا زمانی که نیاز به عبارات Lambda وجود دارد، ترجیح داده می‌شود.


// مثال Method Syntax: انتخاب دانش‌آموزان بالای 20 سال با نمره بالای 80
var selectedStudentsMethodSyntax = students
                                .Where(student => student.Age > 20 && student.Grade > 80)
                                .OrderBy(student => student.Name)
                                .Select(student => new { student.Name, student.Grade });

Console.WriteLine("\nدانش‌آموزان بالای 20 سال با نمره بالای 80 (Method Syntax):");
foreach (var s in selectedStudentsMethodSyntax)
{
    Console.WriteLine($"- نام: {s.Name}, نمره: {s.Grade}");
}

2.3. انتخاب بین Query Syntax و Method Syntax

  • Query Syntax:
    • برای کوئری‌های ساده و شبیه SQL مناسب است.
    • برای کسانی که با SQL آشنا هستند، خوانایی بیشتری دارد.
    • برای عملیات `Join` و `Group By` پیچیده، می‌تواند خواناتر باشد.
  • Method Syntax:
    • انعطاف‌پذیری بیشتری را ارائه می‌دهد، به خصوص هنگام استفاده از عبارات Lambda.
    • برای chaining (زنجیره‌ای کردن) چندین عملیات کوئری مناسب است.
    • برای کوئری‌های پیچیده‌تر که با Query Syntax قابل بیان نیستند یا بسیار طولانی می‌شوند (مثلاً ترکیب عملیات خاص).
    • تنها راه برای استفاده از برخی از عملگرهای LINQ (مانند `FirstOrDefault`, `Count`, `Average`).

در نهایت، هر دو سینتکس معادل یکدیگر هستند و انتخاب بین آن‌ها به ترجیح شخصی، خوانایی کد و نیازهای خاص پروژه بستگی دارد. بسیاری از توسعه‌دهندگان از ترکیبی از هر دو استفاده می‌کنند.

3. اجرای تاخیری (Deferred Execution) در LINQ

یکی از مفاهیم بنیادی و بسیار مهم در LINQ، اجرای تاخیری (Deferred Execution) است. این مفهوم نقش حیاتی در بهینه‌سازی عملکرد و انعطاف‌پذیری کوئری‌های LINQ ایفا می‌کند. برخلاف بسیاری از عملیات‌های سنتی که بلافاصله پس از تعریف اجرا می‌شوند، کوئری‌های LINQ به صورت پیش‌فرض تا زمانی که به نتایج آن‌ها نیاز باشد، اجرا نمی‌شوند.

3.1. مفهوم اجرای تاخیری

وقتی یک کوئری LINQ را تعریف می‌کنید، در واقع یک مجموعه از دستورالعمل‌ها را ایجاد می‌کنید که چگونه داده‌ها باید بازیابی و دستکاری شوند، اما این دستورالعمل‌ها بلافاصله اجرا نمی‌شوند. اجرای واقعی کوئری به تأخیر می‌افتد تا زمانی که شما شروع به پیمایش (Iteration) نتایج کوئری کنید (مثلاً با یک حلقه `foreach`) یا یک عملگر اجباری‌کننده (Forceful Operator) را فراخوانی کنید.

مثال:


List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };

// تعریف کوئری (اجرای تاخیری)
var evenNumbers = from num in numbers
                  where num % 2 == 0
                  select num;

Console.WriteLine("کوئری تعریف شد، اما هنوز اجرا نشده است.");

// کوئری در اینجا (اولین پیمایش) اجرا می‌شود
foreach (var n in evenNumbers)
{
    Console.WriteLine(n);
}

// اگر منبع داده تغییر کند، کوئری در اجرای مجدد نتایج متفاوتی خواهد داد
numbers.Add(6);

Console.WriteLine("\nاجرای مجدد کوئری پس از اضافه شدن عنصر جدید:");
foreach (var n in evenNumbers)
{
    Console.WriteLine(n);
}

در مثال بالا، زمانی که `evenNumbers` تعریف می‌شود، هیچ عملیاتی روی لیست `numbers` انجام نمی‌شود. فقط یک شیء `IEnumerable` (یا `IQueryable` در سناریوهای خاص) برگردانده می‌شود که می‌داند چگونه اعداد زوج را از لیست اصلی استخراج کند. کوئری تنها زمانی اجرا می‌شود که حلقه `foreach` روی `evenNumbers` شروع به کار می‌کند. اگر پس از تعریف کوئری، لیست `numbers` تغییر کند (مثلاً عدد 6 اضافه شود)، در اجرای مجدد کوئری، عدد 6 نیز در نتایج لحاظ خواهد شد.

3.2. مزایای اجرای تاخیری

  • بهینه‌سازی عملکرد:
    • فقط آنچه لازم است، بازیابی می‌شود: در سناریوهای LINQ to SQL/Entities، کوئری‌ها به SQL تبدیل می‌شوند و تنها داده‌های لازم از پایگاه داده بازیابی می‌شوند، نه کل جدول.
    • عملیات زنجیره‌ای: چندین عملگر LINQ می‌توانند زنجیره‌ای شوند. به جای انجام هر عملیات به صورت جداگانه و ایجاد مجموعه‌های میانی، LINQ یک درخت عبارت (Expression Tree) می‌سازد و سپس آن را به یک کوئری بهینه (مثلاً یک SQL statement واحد) تبدیل می‌کند.
  • کاهش مصرف حافظه: مجموعه‌های موقت بزرگ در حافظه ایجاد نمی‌شوند تا زمانی که واقعاً به نتایج نیاز باشد.
  • افزایش انعطاف‌پذیری: می‌توانید کوئری‌ها را به صورت پویا بسازید و تغییر دهید تا زمانی که آماده اجرا شوند.

3.3. عملگرهای اجرای فوری (Immediate Execution Operators)

برخی از عملگرهای LINQ بلافاصله کوئری را اجرا می‌کنند و نتایج را در یک مجموعه جدید (مانند `List` یا آرایه) ذخیره می‌کنند. این عملگرها معمولاً داده‌ها را “مادی” (Materialize) می‌کنند. متداول‌ترین آن‌ها عبارتند از:

  • `ToList()`: نتایج را به `List` تبدیل می‌کند.
  • `ToArray()`: نتایج را به آرایه تبدیل می‌کند.
  • `ToDictionary()`: نتایج را به `Dictionary` تبدیل می‌کند.
  • `Count()`, `Sum()`, `Min()`, `Max()`, `Average()`: این عملگرها نتایج تجمعی را محاسبه می‌کنند و نیاز به پیمایش کامل مجموعه دارند.
  • `First()`, `FirstOrDefault()`, `Single()`, `SingleOrDefault()`: این عملگرها یک عنصر خاص را برمی‌گردانند و کوئری را برای یافتن آن عنصر اجرا می‌کنند.

مثال:


List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };

// اجرای فوری با ToList()
List<int> evenNumbersList = (from num in numbers
                             where num % 2 == 0
                             select num).ToList();

numbers.Add(6); // این تغییر روی evenNumbersList تأثیری ندارد

Console.WriteLine("\nEven numbers using ToList():");
foreach (var n in evenNumbersList)
{
    Console.WriteLine(n);
}

// اجرای فوری با Count()
int count = (from num in numbers
             where num > 3
             select num).Count(); // کوئری در اینجا اجرا می‌شود

Console.WriteLine($"\nCount of numbers greater than 3: {count}");

درک اجرای تاخیری برای نوشتن کوئری‌های LINQ کارآمد، به ویژه هنگام کار با پایگاه‌های داده، بسیار حیاتی است.

4. عملگرهای استاندارد کوئری LINQ (Standard Query Operators)

عملگرهای استاندارد کوئری (Standard Query Operators – SQOs) متدهای توسعه‌ای هستند که بر روی انواع `IEnumerable` پیاده‌سازی شده‌اند و قابلیت‌های قدرتمند LINQ را فراهم می‌کنند. این عملگرها به گروه‌های مختلفی تقسیم می‌شوند که هر کدام وظیفه خاصی را انجام می‌دهند.

4.1. عملگرهای فیلتر (Filtering Operators)

این عملگرها برای انتخاب عناصری از یک مجموعه که با یک شرط خاص مطابقت دارند، استفاده می‌شوند.

  • `Where` (Syntax: `where` clause): فیلتر کردن عناصر بر اساس یک شرط.

// Query Syntax
var youngStudents = from student in students
                    where student.Age < 22
                    select student;

// Method Syntax
var highGrades = students.Where(s => s.Grade > 90);

4.2. عملگرهای مرتب‌سازی (Ordering Operators)

این عملگرها برای مرتب‌سازی عناصر یک مجموعه بر اساس یک یا چند کلید استفاده می‌شوند.

  • `OrderBy` / `OrderByDescending` (Syntax: `orderby` clause): مرتب‌سازی صعودی یا نزولی بر اساس یک کلید.
  • `ThenBy` / `ThenByDescending`: مرتب‌سازی ثانویه (زمانی که چند عنصر دارای کلید مرتب‌سازی اولیه یکسان باشند).

// Query Syntax
var sortedByName = from student in students
                   orderby student.Name ascending
                   select student;

// Method Syntax (نام صعودی، سپس نمره نزولی)
var multiSorted = students.OrderBy(s => s.Name)
                          .ThenByDescending(s => s.Grade);

4.3. عملگرهای پروجکشن (Projection Operators)

این عملگرها برای تبدیل عناصر یک مجموعه به یک شکل جدید یا انتخاب زیرمجموعه‌ای از ویژگی‌های آن‌ها استفاده می‌شوند.

  • `Select` (Syntax: `select` clause): انتخاب یا تبدیل عناصر.

// Query Syntax: انتخاب فقط نام و نمره
var studentNamesAndGrades = from student in students
                            select new { student.Name, student.Grade };

// Method Syntax: تبدیل به رشته‌ای از مشخصات دانش‌آموز
var studentInfoStrings = students.Select(s => $"ID: {s.Id}, Name: {s.Name}, Age: {s.Age}");

4.4. عملگرهای گروه‌بندی (Grouping Operators)

این عملگرها برای گروه‌بندی عناصر یک مجموعه بر اساس یک کلید مشترک استفاده می‌شوند.

  • `GroupBy` (Syntax: `group by` clause): گروه‌بندی عناصر.

// Query Syntax: گروه‌بندی دانش‌آموزان بر اساس سن
var studentsByAge = from student in students
                    group student by student.Age into ageGroup
                    select new
                    {
                        Age = ageGroup.Key,
                        Count = ageGroup.Count(),
                        Students = ageGroup.ToList()
                    };

foreach (var group in studentsByAge)
{
    Console.WriteLine($"سن: {group.Age}, تعداد: {group.Count}");
    foreach (var s in group.Students)
    {
        Console.WriteLine($"  - {s.Name}");
    }
}

// Method Syntax
var studentsByGradeRange = students.GroupBy(s => (int)(s.Grade / 10) * 10); // Group by grade range (e.g., 80-89)

4.5. عملگرهای اتصال (Joining Operators)

این عملگرها برای ترکیب دو مجموعه داده بر اساس یک شرط مشترک استفاده می‌شوند.

  • `Join` (Syntax: `join` clause): اجرای یک Inner Join بین دو مجموعه.
  • `GroupJoin`: اجرای یک Left Outer Join.

public class Course
{
    public int CourseId { get; set; }
    public string CourseName { get; set; }
    public int StudentId { get; set; } // Foreign key
}

List<Course> courses = new List<Course>
{
    new Course { CourseId = 101, CourseName = "ریاضی 1", StudentId = 1 },
    new Course { CourseId = 102, CourseName = "برنامه نویسی C#", StudentId = 2 },
    new Course { CourseId = 103, CourseName = "مدیریت پایگاه داده", StudentId = 1 },
    new Course { CourseId = 104, CourseName = "شبکه های کامپیوتری", StudentId = 4 }
};

// Inner Join (Query Syntax)
var studentCourses = from student in students
                     join course in courses on student.Id equals course.StudentId
                     select new { student.Name, course.CourseName };

// Group Join (Method Syntax) - Equivalent to Left Outer Join
var studentsWithTheirCourses = students.GroupJoin(
    courses,
    student => student.Id,
    course => course.StudentId,
    (student, studentCoursesList) => new
    {
        StudentName = student.Name,
        Courses = studentCoursesList.Select(c => c.CourseName).ToList()
    }
);

4.6. عملگرهای تجمیع (Aggregation Operators)

این عملگرها برای انجام محاسبات تجمعی روی مجموعه‌ها استفاده می‌شوند و یک مقدار واحد را برمی‌گردانند.

  • `Count`: تعداد عناصر را برمی‌گرداند.
  • `Sum`: مجموع مقادیر عددی را محاسبه می‌کند.
  • `Min` / `Max`: کوچکترین/بزرگترین مقدار را پیدا می‌کند.
  • `Average`: میانگین مقادیر را محاسبه می‌کند.
  • `Aggregate`: یک عملیات تجمیع سفارشی را انجام می‌دهد.

int totalStudents = students.Count();
double averageGrade = students.Average(s => s.Grade);
double maxGrade = students.Max(s => s.Grade);

// Aggregate: محاسبه مجموع سن تمام دانش‌آموزان
int totalAge = students.Aggregate(0, (sum, student) => sum + student.Age);

4.7. عملگرهای مقداری (Quantifier Operators)

این عملگرها برای بررسی اینکه آیا عناصری در یک مجموعه با یک شرط خاص مطابقت دارند یا خیر، استفاده می‌شوند و یک مقدار بولی (True/False) برمی‌گردانند.

  • `Any`: اگر حداقل یک عنصر با شرط مطابقت داشته باشد، `True` برمی‌گرداند.
  • `All`: اگر تمام عناصر با شرط مطابقت داشته باشند، `True` برمی‌گرداند.
  • `Contains`: بررسی می‌کند که آیا مجموعه شامل یک عنصر خاص است یا خیر.

bool anyAbove90 = students.Any(s => s.Grade > 90);
bool allAbove70 = students.All(s => s.Grade > 70);
bool containsAli = students.Any(s => s.Name == "علی");

4.8. عملگرهای پارتیشن‌بندی (Partitioning Operators)

این عملگرها برای انتخاب زیرمجموعه‌ای از عناصر از ابتدا یا انتهای یک مجموعه استفاده می‌شوند.

  • `Take`: `n` عنصر اول را برمی‌گرداند.
  • `Skip`: `n` عنصر اول را نادیده می‌گیرد و بقیه را برمی‌گرداند.
  • `TakeWhile` / `SkipWhile`: عناصر را تا زمانی که یک شرط درست باشد (یا تا زمانی که درست نباشد) انتخاب/نادیده می‌گیرد.

var top3Students = students.OrderByDescending(s => s.Grade).Take(3);
var studentsAfterFirstTwo = students.Skip(2);

4.9. عملگرهای مجموعه (Set Operators)

این عملگرها برای انجام عملیات نظریه مجموعه‌ها (مانند اجتماع، اشتراک، تفاضل) استفاده می‌شوند.

  • `Distinct`: عناصر تکراری را حذف می‌کند.
  • `Union`: اجتماع دو مجموعه را برمی‌گرداند (عناصر تکراری حذف می‌شوند).
  • `Intersect`: اشتراک دو مجموعه را برمی‌گرداند.
  • `Except`: تفاضل دو مجموعه را برمی‌گرداند (عناصری که در مجموعه اول هستند اما در دومی نیستند).

List<int> list1 = new List<int> { 1, 2, 3, 4, 5 };
List<int> list2 = new List<int> { 4, 5, 6, 7, 8 };

var distinctNumbers = list1.Distinct();
var unionNumbers = list1.Union(list2);
var intersectNumbers = list1.Intersect(list2);
var exceptNumbers = list1.Except(list2);

4.10. عملگرهای عناصر (Element Operators)

این عملگرها برای بازیابی یک عنصر خاص از یک مجموعه استفاده می‌شوند.

  • `First` / `FirstOrDefault`: اولین عنصر را برمی‌گرداند. `First` اگر عنصری یافت نشود، استثنا (Exception) پرتاب می‌کند؛ `FirstOrDefault` مقدار پیش‌فرض را برمی‌گرداند.
  • `Single` / `SingleOrDefault`: بررسی می‌کند که دقیقاً یک عنصر با شرط مطابقت داشته باشد. `Single` اگر بیش از یک یا هیچ عنصری یافت نشود، استثنا پرتاب می‌کند؛ `SingleOrDefault` مقدار پیش‌فرض را برمی‌گرداند.
  • `ElementAt` / `ElementAtOrDefault`: عنصر در یک ایندکس مشخص را برمی‌گرداند.

var firstStudent = students.First();
var studentWithId3 = students.FirstOrDefault(s => s.Id == 3);
var nonExistentStudent = students.SingleOrDefault(s => s.Name == "ناشناس"); // null

5. LINQ to Objects: کوئری‌نویسی بر روی مجموعه‌های حافظه

LINQ to Objects ابتدایی‌ترین و پرکاربردترین پیاده‌سازی LINQ است که به شما امکان می‌دهد بر روی هر شیئی که رابط `IEnumerable` (مانند `List`, آرایه‌ها، `HashSet` و غیره) را پیاده‌سازی می‌کند، کوئری بگیرید. این نوع LINQ نیازی به پیکربندی خاصی ندارد و به طور مستقیم در حافظه (client-side) عملیات را انجام می‌دهد.

5.1. کار با List<T> و آرایه‌ها

اکثر مثال‌هایی که تا اینجا دیدیم، نمونه‌هایی از LINQ to Objects بودند. این بخش به تفصیل نشان می‌دهد که چگونه می‌توانید از LINQ برای دستکاری مجموعه‌های درون حافظه استفاده کنید.


// تعریف یک لیست از رشته‌ها
List<string> names = new List<string> { "آرش", "بهاره", "سارا", "رضا", "نیما", "سیمین" };

// فیلتر کردن: انتخاب نام‌هایی که با 'س' شروع می‌شوند
var sNames = names.Where(name => name.StartsWith("س"));
Console.WriteLine("نام‌های شروع شده با 'س': " + string.Join(", ", sNames)); // خروجی: سارا, سیمین

// مرتب‌سازی: مرتب‌سازی نام‌ها به صورت نزولی
var sortedNamesDesc = names.OrderByDescending(name => name);
Console.WriteLine("نام‌های مرتب شده نزولی: " + string.Join(", ", sortedNamesDesc)); // خروجی: نیما, سارا, سیمین, رضا, بهاره, آرش

// پروجکشن: انتخاب طول هر نام
var nameLengths = names.Select(name => name.Length);
Console.WriteLine("طول نام‌ها: " + string.Join(", ", nameLengths)); // خروجی: 4, 5, 4, 3, 4, 5

// ترکیب عملیات: نام‌هایی با طول بیشتر از 4 و مرتب شده صعودی
var longNamesSorted = names.Where(name => name.Length > 4)
                           .OrderBy(name => name);
Console.WriteLine("نام‌های طولانی و مرتب شده: " + string.Join(", ", longNamesSorted)); // خروجی: بهاره, سیمین

5.2. استفاده از Anonymous Types و Tuple ها

LINQ اغلب با انواع ناشناس (Anonymous Types) برای پروجکشن داده‌ها استفاده می‌شود. این انواع به شما امکان می‌دهند یک شیء جدید با مجموعه‌ای از ویژگی‌های دلخواه ایجاد کنید بدون اینکه نیاز به تعریف یک کلاس جداگانه داشته باشید.


// ایجاد یک لیست از اشیاء با استفاده از Anonymous Type
var products = new[]
{
    new { Name = "لپ‌تاپ", Price = 1200.0, Category = "الکترونیک" },
    new { Name = "ماوس", Price = 25.0, Category = "لوازم جانبی" },
    new { Name = "کیبورد", Price = 75.0, Category = "لوازم جانبی" },
    new { Name = "تلویزیون", Price = 800.0, Category = "الکترونیک" }
};

// انتخاب محصولات الکترونیک با قیمت بالای 500
var expensiveElectronics = products.Where(p => p.Category == "الکترونیک" && p.Price > 500)
                                   .Select(p => new { p.Name, p.Price }); // ایجاد Anonymous Type جدید

foreach (var item in expensiveElectronics)
{
    Console.WriteLine($"محصول: {item.Name}, قیمت: {item.Price}");
}

// استفاده از Tuples (C# 7.0 به بالا)
var productTuples = products.Select(p => (p.Name, p.Price)); // ایجاد ValueTuple
Console.WriteLine($"\nاولین محصول به صورت Tuple: {productTuples.First().Name}, {productTuples.First().Price}");

5.3. عملگر `OfType` و `Cast`

این عملگرها برای فیلتر کردن یا تبدیل عناصر یک مجموعه به یک نوع خاص استفاده می‌شوند. `OfType` تنها عناصری را برمی‌گرداند که قابل تبدیل به نوع `T` باشند، در حالی که `Cast` اگر عنصری قابل تبدیل نباشد، استثنا پرتاب می‌کند.


List<object> mixedList = new List<object> { 1, "hello", 2.5, "world", 10, true };

// انتخاب فقط رشته‌ها
var stringsOnly = mixedList.OfType<string>();
Console.WriteLine("رشته‌ها: " + string.Join(", ", stringsOnly)); // خروجی: hello, world

// انتخاب فقط اعداد صحیح
var intsOnly = mixedList.OfType<int>();
Console.WriteLine("اعداد صحیح: " + string.Join(", ", intsOnly)); // خروجی: 1, 10

6. LINQ to XML: کارآمدی در دستکاری اسناد XML

LINQ to XML مجموعه‌ای از APIهای جدید و مدرن برای کار با اسناد XML در .NET است که از قابلیت‌های LINQ بهره می‌برد. این APIها (در فضای نام `System.Xml.Linq`) جایگزین مدل‌های قدیمی‌تر مانند `XmlDocument` و `XmlReader/Writer` شده‌اند و یک رویکرد شی‌گرا و بسیار شهودی برای ایجاد، خواندن، دستکاری و ذخیره اسناد XML ارائه می‌دهند.

6.1. مدل شی‌گرای LINQ to XML: XDocument, XElement, XAttribute

هسته LINQ to XML بر پایه کلاس‌های اصلی زیر بنا شده است:

  • `XDocument`: نمایانگر کل یک سند XML است (همانند `XmlDocument`).
  • `XElement`: نمایانگر یک عنصر XML است (مانند یک گره).
  • `XAttribute`: نمایانگر یک صفت XML است.
  • `XText`, `XComment`, `XProcessingInstruction`, `XCData`: نمایانگر انواع مختلف محتوا در XML.

این کلاس‌ها ساختاری درختی را تشکیل می‌دهند که امکان پیمایش و کوئری‌نویسی با LINQ را فراهم می‌آورد.

6.2. ایجاد اسناد XML

LINQ to XML امکان ایجاد اسناد XML را به روشی بسیار روان و تابعی (Functional Construction) فراهم می‌کند.


using System.Xml.Linq;

// ایجاد یک سند XML ساده
XDocument doc = new XDocument(
    new XElement("Books",
        new XElement("Book", new XAttribute("id", "b101"),
            new XElement("Title", "آموزش LINQ"),
            new XElement("Author", "رضا احمدی"),
            new XElement("Price", 35.00)
        ),
        new XElement("Book", new XAttribute("id", "b102"),
            new XElement("Title", "پایتون پیشرفته"),
            new XElement("Author", "سارا حسینی"),
            new XElement("Price", 45.50)
        )
    )
);

doc.Save("Books.xml");
Console.WriteLine("فایل Books.xml ایجاد شد.");

6.3. بارگذاری و کوئری گرفتن از اسناد XML

پس از بارگذاری یک سند XML، می‌توانید با استفاده از عملگرهای استاندارد LINQ بر روی آن کوئری بگیرید. متدهای `Elements()`, `Attribute()`, `Descendants()`, `Ancestors()` از جمله مهم‌ترین متدها برای پیمایش درخت XML هستند.


// بارگذاری سند XML
XDocument loadedDoc = XDocument.Load("Books.xml");

// انتخاب عنوان تمام کتاب‌ها
var titles = from book in loadedDoc.Descendants("Book")
             select book.Element("Title").Value;

Console.WriteLine("\nعناوین کتاب‌ها:");
foreach (var title in titles)
{
    Console.WriteLine($"- {title}");
}

// انتخاب کتاب‌هایی با قیمت بالای 40
var expensiveBooks = from book in loadedDoc.Descendants("Book")
                     where (double)book.Element("Price") > 40
                     select new
                     {
                         Id = book.Attribute("id").Value,
                         Title = book.Element("Title").Value,
                         Author = book.Element("Author").Value
                     };

Console.WriteLine("\nکتاب‌های گران‌تر از 40:");
foreach (var book in expensiveBooks)
{
    Console.WriteLine($"- ID: {book.Id}, عنوان: {book.Title}, نویسنده: {book.Author}");
}

// انتخاب یک عنصر خاص بر اساس ویژگی
var bookById = loadedDoc.Descendants("Book")
                        .FirstOrDefault(b => b.Attribute("id")?.Value == "b101");
if (bookById != null)
{
    Console.WriteLine($"\nکتاب با ID b101: {bookById.Element("Title").Value}");
}

6.4. دستکاری اسناد XML

LINQ to XML همچنین روش‌های آسانی برای افزودن، حذف یا تغییر عناصر و ویژگی‌ها فراهم می‌کند.


// افزودن یک کتاب جدید
loadedDoc.Element("Books").Add(
    new XElement("Book", new XAttribute("id", "b103"),
        new XElement("Title", "دیتابیس NoSQL"),
        new XElement("Author", "مریم نوری"),
        new XElement("Price", 50.00)
    )
);

// تغییر قیمت یک کتاب
loadedDoc.Descendants("Book")
         .Where(b => b.Attribute("id")?.Value == "b101")
         .FirstOrDefault()
         ?.SetElementValue("Price", 30.00); // تغییر قیمت آموزش LINQ

// حذف یک کتاب
loadedDoc.Descendants("Book")
         .Where(b => b.Attribute("id")?.Value == "b102")
         .Remove(); // حذف پایتون پیشرفته

loadedDoc.Save("UpdatedBooks.xml");
Console.WriteLine("\nفایل UpdatedBooks.xml ایجاد شد.");

LINQ to XML کار با XML را به طرز چشمگیری ساده‌تر و قابل فهم‌تر می‌کند و آن را به ابزاری قدرتمند برای سناریوهایی که نیاز به پردازش XML دارید تبدیل می‌کند.

7. LINQ to SQL و LINQ to Entities (Entity Framework): پلی به پایگاه داده

یکی از قوی‌ترین کاربردهای LINQ، در تعامل با پایگاه‌های داده است. LINQ to SQL (یک ORM سبک برای SQL Server) و به خصوص LINQ to Entities (بخشی از Entity Framework، یک ORM کامل و منعطف) به توسعه‌دهندگان اجازه می‌دهند تا کوئری‌های SQL را با استفاده از سینتکس LINQ و مدل‌های شی‌گرای C# بنویسند.

7.1. مفهوم ORM و نقش LINQ

ORM (Object-Relational Mapper) ابزاری است که شکاف بین مدل‌های شی‌گرای زبان برنامه‌نویسی و مدل‌های رابطه‌ای پایگاه داده را پر می‌کند. به جای نوشتن کوئری‌های SQL به صورت رشته‌ای، ORM به شما امکان می‌دهد با اشیاء C# (که به جداول پایگاه داده نگاشت شده‌اند) کار کنید و ORM مسئول ترجمه عملیات‌های LINQ شما به دستورات SQL مناسب و اجرای آن‌ها در پایگاه داده است.

نقش LINQ در اینجا ارائه یک زبان کوئری یکپارچه برای ORM است. توسعه‌دهنده کوئری LINQ را می‌نویسد و Provider مربوطه (مثلاً Entity Framework Core) آن را به SQL معادل ترجمه کرده و به پایگاه داده ارسال می‌کند.

7.2. LINQ to SQL: یک رویکرد ساده‌تر

LINQ to SQL یک پیاده‌سازی سبک‌تر و قدیمی‌تر از LINQ برای تعامل با پایگاه داده SQL Server است. اگرچه هنوز در پروژه‌های قدیمی‌تر استفاده می‌شود، اما Entity Framework Core به عنوان راه حل پیش‌فرض و جامع‌تر مایکروسافت برای دسترسی به داده‌ها در .NET Core و .NET 5+ شناخته می‌شود.

نحوه کار با LINQ to SQL شامل ایجاد یک DataContext است که نمایانگر پایگاه داده شماست و سپس تعریف کلاس‌هایی که جداول پایگاه داده را نگاشت می‌کنند. کوئری‌ها بر روی این کلاس‌ها نوشته می‌شوند.


// مثال مفهومی برای LINQ to SQL (نیاز به تعریف DataContext و کلاس‌های مدل)
// فرض کنید یک کلاس Product داریم که به جدول Products نگاشت شده است.
// DataContext db = new DataContext("YourConnectionString");
// Table<Product> Products = db.GetTable<Product>();

// var expensiveProducts = from p in Products
//                         where p.Price > 100
//                         select p;

// foreach (var product in expensiveProducts)
// {
//     Console.WriteLine($"Product: {product.Name}, Price: {product.Price}");
// }

7.3. LINQ to Entities با Entity Framework Core

Entity Framework Core (EF Core) ORM پیشنهادی مایکروسافت است که از LINQ برای کوئری‌نویسی استفاده می‌کند. EF Core از طیف وسیعی از پایگاه‌های داده (SQL Server, SQLite, PostgreSQL, MySQL و…) پشتیبانی می‌کند و امکانات قدرتمندی مانند Migration، ردیابی تغییرات، و تراکنش‌ها را ارائه می‌دهد.

برای استفاده از LINQ با EF Core، شما یک کلاس `DbContext` تعریف می‌کنید که نمایانگر نشست پایگاه داده شماست و شامل `DbSet`ها برای هر جدول است.


// فرض کنید کلاس‌های Student و Course را از قبل تعریف کرده‌ایم و به پایگاه داده نگاشت شده‌اند.
// و DbContext به نام ApplicationDbContext داریم.

// using Microsoft.EntityFrameworkCore;
// using System.Linq;

// public class ApplicationDbContext : DbContext
// {
//     public DbSet<Student> Students { get; set; }
//     public DbSet<Course> Courses { get; set; }

//     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
//     {
//         optionsBuilder.UseSqlServer("YourConnectionString");
//     }
// }

// مثال عملی: (نیاز به یک کانتکست فعال و یک پایگاه داده متصل)
// using (var context = new ApplicationDbContext())
// {
//     // SELECT * FROM Students WHERE Age > 20
//     var oldStudents = context.Students.Where(s => s.Age > 20).ToList();

//     Console.WriteLine("\nدانش‌آموزان بالای 20 سال (EF Core):");
//     foreach (var student in oldStudents)
//     {
//         Console.WriteLine($"- نام: {student.Name}, سن: {student.Age}");
//     }

//     // SELECT TOP 1 * FROM Students ORDER BY Grade DESC
//     var topStudent = context.Students.OrderByDescending(s => s.Grade).FirstOrDefault();
//     if (topStudent != null)
//     {
//         Console.WriteLine($"\nدانش‌آموز برتر: {topStudent.Name}, نمره: {topStudent.Grade}");
//     }

//     // INSERT
//     // var newStudent = new Student { Name = "امین", Age = 24, Grade = 89.0 };
//     // context.Students.Add(newStudent);
//     // context.SaveChanges();
//     // Console.WriteLine($"دانش‌آموز {newStudent.Name} اضافه شد.");

//     // UPDATE
//     // var studentToUpdate = context.Students.FirstOrDefault(s => s.Name == "امین");
//     // if (studentToUpdate != null)
//     // {
//     //     studentToUpdate.Grade = 91.0;
//     //     context.SaveChanges();
//     //     Console.WriteLine($"نمره امین به 91 تغییر یافت.");
//     // }

//     // DELETE
//     // var studentToDelete = context.Students.FirstOrDefault(s => s.Name == "امین");
//     // if (studentToDelete != null)
//     // {
//     //     context.Students.Remove(studentToDelete);
//     //     context.SaveChanges();
//     //     Console.WriteLine($"امین حذف شد.");
//     // }
// }

7.4. ملاحظات کارایی در LINQ to Entities

هنگام استفاده از LINQ to Entities، درک اینکه چگونه LINQ به SQL ترجمه می‌شود بسیار مهم است:

  • `IQueryable` در برابر `IEnumerable`: هنگامی که کوئری‌ها را بر روی `DbSet`ها می‌نویسید، آن‌ها `IQueryable` را برمی‌گردانند. این به EF Core اجازه می‌دهد تا کوئری LINQ را به یک درخت عبارت تبدیل کرده و سپس آن را به SQL بهینه ترجمه کند. اگر در میانه زنجیره کوئری از متدی استفاده کنید که `IEnumerable` برمی‌گرداند (مانند `ToList()`, `AsEnumerable()`), بقیه کوئری در حافظه برنامه اجرا خواهد شد که می‌تواند منجر به بازیابی بیش از حد داده از پایگاه داده (N+1 problem) و مشکلات عملکردی شود. همیشه سعی کنید فیلترینگ و مرتب‌سازی را تا حد امکان در سمت پایگاه داده (یعنی قبل از Materialize کردن با `ToList()` یا `ToArray()`) انجام دهید.
  • انتخاب (Projection) بهینه: فقط ستون‌هایی را انتخاب کنید که واقعاً نیاز دارید (`Select` کردن به یک نوع ناشناس یا DTO) تا پهنای باند شبکه و مصرف حافظه را کاهش دهید.
  • بارگذاری حریصانه (Eager Loading) و بارگذاری تنبل (Lazy Loading): برای بارگذاری داده‌های مرتبط، به جای پیمایش دستی و ایجاد N+1 کوئری، از `Include()` (برای eager loading) یا پیکربندی lazy loading استفاده کنید.
  • پروفایلینگ کوئری‌ها: از ابزارهای پروفایلینگ (مانند SQL Server Profiler یا ابزارهای داخلی EF Core) برای مشاهده SQL تولید شده توسط EF Core استفاده کنید. این کار به شما کمک می‌کند تا کوئری‌های ناکارآمد را شناسایی و بهینه‌سازی کنید.

8. مفاهیم پیشرفته و کاربردهای خاص LINQ

LINQ فراتر از عملیات‌های پایه، دارای مفاهیم پیشرفته‌ای است که قدرت و انعطاف‌پذیری آن را دوچندان می‌کند.

8.1. عبارات Lambda (Lambda Expressions)

عبارات Lambda بلوک‌های کد فشرده‌ای هستند که می‌توانند به عنوان آرگومان برای متدها (مانند عملگرهای LINQ در Method Syntax) ارسال شوند یا برای ایجاد نماینده‌ها (Delegates) استفاده شوند. آن‌ها ستون فقرات Method Syntax در LINQ هستند.


// Lambda Expression for filtering
var activeStudents = students.Where(s => s.Age < 25);

// Lambda Expression for ordering
var studentsByGrade = students.OrderBy(s => s.Grade);

// Lambda Expression for projection
var studentAges = students.Select(s => s.Age);

8.2. درختان عبارت (Expression Trees)

درختان عبارت، ساختارهای داده‌ای هستند که کد را به صورت درختی نمایش می‌دهند. هنگامی که یک کوئری LINQ بر روی یک منبع داده `IQueryable` نوشته می‌شود (مانند LINQ to Entities)، کامپایلر C# آن کوئری را به جای IL (Intermediate Language) به یک درخت عبارت تبدیل می‌کند. این درخت عبارت سپس توسط Provider مربوطه (مثلاً EF Core) قابل تجزیه و تحلیل است تا به یک زبان کوئری بومی (مانند SQL) ترجمه شود.

این مکانیسم چیزی است که به LINQ امکان می‌دهد با پایگاه‌های داده و سایر منابع خارجی به طور کارآمد تعامل داشته باشد، زیرا کوئری در سمت سرور اجرا می‌شود، نه در حافظه برنامه.

8.3. Parallel LINQ (PLINQ)

PLINQ (Parallel LINQ) یک پیاده‌سازی LINQ است که به شما امکان می‌دهد کوئری‌های خود را به صورت موازی (Parallel) اجرا کنید و از تمام هسته‌های پردازنده موجود برای بهبود عملکرد بر روی مجموعه‌های بزرگ داده بهره‌مند شوید. برای استفاده از PLINQ، کافی است متد `AsParallel()` را به مجموعه خود اضافه کنید.


List<long> largeNumbers = Enumerable.Range(1, 100_000_000).Select(x => (long)x).ToList();

// اجرای عادی
var sumSequential = largeNumbers.Where(n => n % 2 == 0).Sum();
Console.WriteLine($"\nمجموع اعداد زوج (ترتیبی): {sumSequential}");

// اجرای موازی با PLINQ
var sumParallel = largeNumbers.AsParallel().Where(n => n % 2 == 0).Sum();
Console.WriteLine($"مجموع اعداد زوج (موازی): {sumParallel}");

هرچند PLINQ می‌تواند عملکرد را بهبود بخشد، اما استفاده از آن باید با دقت انجام شود. سربار (overhead) موازی‌سازی می‌تواند برای مجموعه‌های کوچک، از مزایای عملکردی بیشتر باشد. همچنین، ترتیب عناصر ممکن است در نتایج موازی تغییر کند، مگر اینکه از عملگرهای مرتب‌سازی خاص استفاده شود.

8.4. Extension Methods سفارشی

شما می‌توانید Extension Methodهای خود را برای `IEnumerable` بنویسید و عملگرهای LINQ سفارشی ایجاد کنید. این کار به شما امکان می‌دهد تا عملیات‌های رایج و پیچیده را کپسوله کرده و کد خود را ماژولارتر و قابل استفاده‌تر کنید.


public static class CustomLinqExtensions
{
    // Extension Method برای فیلتر کردن دانش‌آموزان با نمره بالا
    public static IEnumerable<Student> WithHighGrade(this IEnumerable<Student> students, double minGrade)
    {
        return students.Where(s => s.Grade >= minGrade);
    }

    // Extension Method برای انتخاب فقط نام کامل (اگر داشته باشند)
    public static IEnumerable<string> SelectFullName(this IEnumerable<Student> students)
    {
        return students.Select(s => s.Name); // فرض می‌کنیم Name نام کامل است یا قابل ترکیب است
    }
}

// استفاده از Extension Methodهای سفارشی
// var topPerformers = students.WithHighGrade(90).SelectFullName();
// Console.WriteLine("\nدانش‌آموزان با عملکرد بالا: " + string.Join(", ", topPerformers));

9. بهترین شیوه‌ها و نکات عملکردی در LINQ

برای استفاده موثر و بهینه از LINQ، رعایت برخی بهترین شیوه‌ها و نکات عملکردی ضروری است، به ویژه در پروژه‌های بزرگ و برنامه‌های کاربردی حساس به عملکرد.

9.1. همیشه فیلتر کنید، بعد انتخاب کنید (Filter Early, Select Late)

این یک قانون طلایی در LINQ است. همیشه ابتدا داده‌های خود را فیلتر کنید تا تعداد عناصری که باید روی آن‌ها عملیات انجام دهید کاهش یابد، سپس عملیات پروجکشن (`Select`) یا مرتب‌سازی (`OrderBy`) را انجام دهید. این کار به ویژه هنگام کار با پایگاه‌های داده (LINQ to Entities) حیاتی است، زیرا به EF Core اجازه می‌دهد تا یک کوئری SQL بهینه با عبارت `WHERE` در ابتدای آن تولید کند و از بازیابی داده‌های غیرضروری جلوگیری کند.


// بد: اول همه داده‌ها را انتخاب کن، بعد فیلتر کن (ممکن است تمام ستون‌ها را بکشد!)
// var badExample = context.Students.Select(s => new { s.Name, s.Age, s.Grade }).Where(s => s.Age > 20);

// خوب: اول فیلتر کن، بعد فقط داده‌های لازم را انتخاب کن.
// var goodExample = context.Students.Where(s => s.Age > 20).Select(s => new { s.Name, s.Age, s.Grade });

9.2. پروجکشن تنها به داده‌های مورد نیاز

هنگام استفاده از `Select`، فقط ویژگی‌هایی را که واقعاً نیاز دارید، انتخاب کنید. بازیابی تمام ستون‌های یک جدول فقط برای استفاده از چند مورد، به ویژه در پایگاه داده، سربار شبکه و حافظه را افزایش می‌دهد. از انواع ناشناس (Anonymous Types) یا DTO (Data Transfer Objects) برای این منظور استفاده کنید.


// bad: fetches all columns of Student
// var allStudents = context.Students.ToList();

// good: fetches only Name and Grade
// var studentDetails = context.Students.Select(s => new { s.Name, s.Grade }).ToList();

9.3. استفاده صحیح از `IQueryable` و `IEnumerable`

همانطور که قبلاً بحث شد، درک تفاوت بین `IQueryable` (اجرای کوئری در سمت منبع داده) و `IEnumerable` (اجرای کوئری در حافظه برنامه) بسیار مهم است. همیشه سعی کنید عملیات فیلترینگ و مرتب‌سازی را تا زمانی که ممکن است با `IQueryable` انجام دهید تا از ترجمه بهینه به SQL اطمینان حاصل کنید.


// IQueryable - Operations will be translated to SQL
// var query = context.Students.Where(s => s.Age > 20);

// If you call ToList() here, the query is executed against the database.
// var studentsList = query.ToList();

// Bad: calling ToList() too early, subsequent operations run in memory
// var studentsInMemory = context.Students.ToList().Where(s => s.Age > 20);

// Good: filter first, then materialize
// var studentsFromDb = context.Students.Where(s => s.Age > 20).ToList();

9.4. جلوگیری از N+1 Problem

این مشکل زمانی رخ می‌دهد که برای هر آیتم از یک مجموعه اصلی، یک کوئری جداگانه برای بارگذاری داده‌های مرتبط با آن اجرا شود. در Entity Framework، این مشکل اغلب با بارگذاری تنبل (Lazy Loading) نادرست ایجاد می‌شود. از `Include()` برای بارگذاری حریصانه (Eager Loading) داده‌های مرتبط در یک کوئری واحد استفاده کنید.


// Assume Student has a collection of Enrollments and Enrollment has a Course
// Bad (N+1):
// foreach (var student in context.Students.ToList()) // Loads all students
// {
//     foreach (var enrollment in student.Enrollments) // N queries for enrollments
//     {
//         Console.WriteLine(enrollment.Course.Title); // N*M queries for courses if lazy loading is on for Course
//     }
// }

// Good (Eager Loading with Include):
// var studentsWithCourses = context.Students
//                                  .Include(s => s.Enrollments)
//                                  .ThenInclude(e => e.Course)
//                                  .ToList(); // All data loaded in one or few queries

// foreach (var student in studentsWithCourses)
// {
//     foreach (var enrollment in student.Enrollments)
//     {
//         Console.WriteLine(enrollment.Course.Title);
//     }
// }

9.5. استفاده از `AsNoTracking()` برای کوئری‌های فقط خواندنی

در Entity Framework، اگر فقط نیاز به خواندن داده‌ها دارید و نمی‌خواهید آن‌ها را تغییر دهید، از `AsNoTracking()` استفاده کنید. این متد به EF Core دستور می‌دهد که اشیاء بازیابی شده را ردیابی نکند، که می‌تواند سربار حافظه و CPU را کاهش دهد.


// var readOnlyStudents = context.Students.AsNoTracking().Where(s => s.Age > 20).ToList();

9.6. پروفایلینگ و بهینه‌سازی کوئری‌های SQL تولید شده

مهمترین گام در بهینه‌سازی عملکرد LINQ در پایگاه داده، بررسی SQL واقعی است که توسط ORM شما تولید می‌شود. ابزارهایی مانند SQL Server Profiler، MiniProfiler، یا Logging در EF Core به شما امکان می‌دهند کوئری‌های تولید شده را ببینید و اگر ناکارآمد هستند، کوئری LINQ خود را بازنویسی کنید.

9.7. استفاده از `var`

استفاده از کلمه کلیدی `var` برای تعریف متغیرهایی که نتایج کوئری‌های LINQ را نگه می‌دارند، نه تنها کد را خواناتر می‌کند (زیرا نوع دقیق اغلب پیچیده و طولانی است)، بلکه در صورت استفاده از انواع ناشناس، تنها راه ممکن است.


var queryResult = from student in students
                  where student.Age > 20
                  select new { Name = student.Name, AverageGrade = student.Grade };

نتیجه‌گیری: قدرت و انعطاف‌پذیری LINQ

LINQ بیش از یک ابزار کوئری‌نویسی صرف است؛ این یک تغییر پارادایم در نحوه تعامل توسعه‌دهندگان C# با داده‌ها است. با ارائه یک سینتکس یکپارچه، امن از نظر تایپ و قابل Debug، LINQ پیچیدگی کار با منابع داده متنوع را به طرز چشمگیری کاهش می‌دهد و به شما امکان می‌دهد کد تمیزتر، کارآمدتر و قابل نگهداری‌تری بنویسید.

از کوئری‌نویسی بر روی مجموعه‌های ساده در حافظه (LINQ to Objects) گرفته تا دستکاری اسناد XML (LINQ to XML) و برقراری ارتباط قدرتمند با پایگاه‌های داده رابطه‌ای از طریق Entity Framework (LINQ to Entities)، LINQ خود را به عنوان یک ستون فقرات ضروری در توسعه مدرن .NET تثبیت کرده است. درک عمیق مفاهیمی مانند اجرای تاخیری، انواع عملگرهای استاندارد، و بهترین شیوه‌های عملکرد، برای تسلط بر LINQ و بهره‌برداری کامل از پتانسیل آن ضروری است.

با بکارگیری LINQ، شما نه تنها بهره‌وری خود را افزایش می‌دهید، بلکه کدی تولید می‌کنید که خواناتر، قابل اطمینان‌تر و قابل تکامل است. این ویژگی‌ها در دنیای پویا و داده‌محور امروز، بیش از هر زمان دیگری حیاتی هستند.

“تسلط به برنامه‌نویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT”

قیمت اصلی 2.290.000 ریال بود.قیمت فعلی 1.590.000 ریال است.

"تسلط به برنامه‌نویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT"

"با شرکت در این دوره جامع و کاربردی، به راحتی مهارت‌های برنامه‌نویسی پایتون را از سطح مبتدی تا پیشرفته با کمک هوش مصنوعی ChatGPT بیاموزید. این دوره، با بیش از 6 ساعت محتوای آموزشی، شما را قادر می‌سازد تا به سرعت الگوریتم‌های پیچیده را درک کرده و اپلیکیشن‌های هوشمند ایجاد کنید. مناسب برای تمامی سطوح با زیرنویس فارسی حرفه‌ای و امکان دانلود و تماشای آنلاین."

ویژگی‌های کلیدی:

بدون نیاز به تجربه قبلی برنامه‌نویسی

زیرنویس فارسی با ترجمه حرفه‌ای

۳۰ ٪ تخفیف ویژه برای دانشجویان و دانش آموزان