فصل نهم: LINQ Operators

این فصل به بررسی تک‌تک عملگرهای LINQ می‌پردازد. علاوه بر اینکه به‌عنوان یک مرجع عمل می‌کند، دو بخش «Projecting» (در صفحه ۴۷۳) و «Joining» (در صفحه ۴۷۳) مفاهیم مهمی را پوشش می‌دهند:


🔤 مثال پایه

تمامی مثال‌های این فصل فرض می‌کنند که یک آرایه از نام‌ها تعریف شده است:

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };

مثال‌هایی که مربوط به پایگاه‌داده هستند فرض می‌کنند شیء زیر ساخته شده است:

var dbContext = new NutshellContext();

که کلاس NutshellContext به شکل زیر تعریف شده است:

public class NutshellContext : DbContext
{
    public DbSet<Customer> Customers { get; set; }
    public DbSet<Purchase> Purchases { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Customer>(entity =>
        {
            entity.ToTable("Customer");
            entity.Property(e => e.Name).IsRequired();  // ستون غیرقابل تهی
        });

        modelBuilder.Entity<Purchase>(entity =>
        {
            entity.ToTable("Purchase");
            entity.Property(e => e.Date).IsRequired();
            entity.Property(e => e.Description).IsRequired();
        });
    }
}

🧑‍💻 تعریف کلاس‌ها

public class Customer
{
    public int ID { get; set; }
    public string Name { get; set; }
    public virtual List<Purchase> Purchases { get; set; }
        = new List<Purchase>();
}

public class Purchase
{        
    public int ID { get; set; }
    public int? CustomerID { get; set; }
    public DateTime Date { get; set; }
    public string Description { get; set; }
    public decimal Price { get; set; }
    public virtual Customer Customer { get; set; }
}

🛠 ابزار LINQPad

تمامی مثال‌های این فصل در LINQPad از پیش بارگذاری شده‌اند، همراه با یک پایگاه‌داده نمونه که Schema مشابهی دارد.
📥 می‌توانید LINQPad را از www.linqpad.net دانلود کنید.


🗄 تعریف جدول‌های SQL Server متناظر

CREATE TABLE Customer (
    ID int NOT NULL IDENTITY PRIMARY KEY,
    Name nvarchar(30) NOT NULL
)

CREATE TABLE Purchase (
    ID int NOT NULL IDENTITY PRIMARY KEY,
    CustomerID int NOT NULL REFERENCES Customer(ID),
    Date datetime NOT NULL,
    Description nvarchar(30) NOT NULL,
    Price decimal NOT NULL
)

🔎 مرور کلی (Overview)

در این بخش، یک مرور کلی بر عملگرهای استاندارد کوئری ارائه می‌دهیم. این عملگرها در سه دسته تقسیم می‌شوند:

  1. 📌 Sequence in, sequence out (sequence → sequence)
    ➝ یعنی ورودی یک دنباله (sequence) است و خروجی هم یک دنباله.
  2. 📌 Sequence in, single element or scalar value out
    ➝ یعنی ورودی یک دنباله است اما خروجی فقط یک عنصر یا یک مقدار منفرد.
  3. 📌 Nothing in, sequence out (generation methods)
    ➝ یعنی هیچ ورودی وجود ندارد اما خروجی یک دنباله تولید می‌شود.

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


🔄 Sequence → Sequence

بیشتر عملگرهای LINQ در این دسته قرار می‌گیرند. آن‌ها یک یا چند دنباله را به‌عنوان ورودی می‌گیرند و در خروجی یک دنباله تولید می‌کنند.

📊 شکل ۹-۱ عملگرهایی را نشان می‌دهد که ساختار دنباله‌ها را تغییر می‌دهند.

Conventions-UsedThis-Book

📖 عملگرهای LINQ – دسته‌بندی‌ها

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


🔍 Filtering (فیلتر کردن)

ورودی: IEnumerable<TSource>
خروجی: IEnumerable<TSource>

🔹 وظیفه: برگرداندن یک زیرمجموعه از عناصر اصلی.

📌 عملگرها:
Where, Take, TakeLast, TakeWhile, Skip, SkipLast, SkipWhile,
Distinct, DistinctBy


🎨 Projecting (تبدیل/نمایش)

ورودی: IEnumerable<TSource>
خروجی: IEnumerable<TResult>

🔹 وظیفه: تغییر شکل هر عنصر با استفاده از یک lambda function.

📌 عملگرها:
Select, SelectMany


🔗 Joining (اتصال/ترکیب)

ورودی:
IEnumerable<TOuter>, IEnumerable<TInner>
خروجی:
IEnumerable<TResult>

🔹 وظیفه: ترکیب عناصر یک دنباله با دنباله‌ای دیگر.

📌 عملگرها:
Join, GroupJoin, Zip


📑 Ordering (مرتب‌سازی)

ورودی: IEnumerable<TSource>
خروجی: IOrderedEnumerable<TSource>

🔹 وظیفه: بازگرداندن یک دنباله با ترتیب جدید.

📌 عملگرها:
OrderBy, OrderByDescending, ThenBy, ThenByDescending, Reverse


🗂 Grouping (گروه‌بندی)

ورودی: IEnumerable<TSource>
خروجی:

🔹 وظیفه: تقسیم یک دنباله به زیر‌دنباله‌ها.

📌 عملگرها:
GroupBy, Chunk


🔀 Set Operators (عملگرهای مجموعه‌ای)

ورودی:
IEnumerable<TSource>, IEnumerable<TSource>
خروجی:
IEnumerable<TSource>

🔹 وظیفه: گرفتن دو دنباله هم‌نوع و برگرداندن اشتراک، اجتماع یا تفاوت آن‌ها.

📌 عملگرها:
Concat, Union, UnionBy, Intersect, IntersectBy, Except, ExceptBy


🔄 Conversion Methods (تبدیل)

🛠 Import

ورودی: IEnumerable
خروجی: IEnumerable<TResult>

📌 عملگرها:
OfType, Cast

📤 Export

ورودی: IEnumerable<TSource>
خروجی: یک آرایه، لیست، دیکشنری، Lookup یا دنباله

📌 عملگرها:
ToArray, ToList, ToDictionary, ToLookup, AsEnumerable, AsQueryable


🎯 Sequence → Element or Value

🔹 Element Operators (انتخاب عنصر)

ورودی: IEnumerable<TSource>
خروجی: TSource

📌 عملگرها:
First, FirstOrDefault, Last, LastOrDefault,
Single, SingleOrDefault, ElementAt, ElementAtOrDefault,
MinBy, MaxBy, DefaultIfEmpty


🔹 Aggregation Methods (تجمیع)

ورودی: IEnumerable<TSource>
خروجی: یک مقدار منفرد (scalar)

📌 وظیفه: انجام محاسبه روی یک دنباله و بازگرداندن یک مقدار عددی یا مشابه آن.

📌 عملگرها:
Aggregate, Average, Count, LongCount, Sum, Max, Min


🔹 Quantifiers (کوانتیفایرها)

ورودی: IEnumerable<TSource>
خروجی: bool

📌 وظیفه: برگرداندن نتیجه true/false به‌عنوان یک تجمیع.

📌 عملگرها:
All, Any, Contains, SequenceEqual


🌀 Void → Sequence

🔹 Generation Methods (تولید)

ورودی: void
خروجی: IEnumerable<TResult>

📌 وظیفه: ساخت یک دنباله ساده از صفر.

📌 عملگرها:
Empty, Range, Repeat

Conventions-UsedThis-Book

📖 نکته درباره ستون «SQL equivalents»

در جدول‌های مرجع این فصل، ستون «SQL equivalents» لزوماً همان چیزی نیست که یک پیاده‌سازی IQueryable مثل EF Core تولید می‌کند.
بلکه این ستون نشان می‌دهد اگر خودتان می‌خواستید معادل آن کوئری را به زبان SQL بنویسید، معمولاً از چه چیزی استفاده می‌کردید.


🧑‍💻 پیاده‌سازی Enumerable

وقتی کد پیاده‌سازی برای Enumerable نشان داده می‌شود، بررسی موارد زیر در آن حذف شده‌اند:


🔍 درباره متدهای Filtering

در هر یک از متدهای Filtering، همیشه خروجی شامل همان تعداد یا کمتر از عناصری است که در ورودی داشتید.
⚠️ هیچ‌وقت نمی‌توانید عناصر بیشتری از آنچه وارد کرده‌اید به دست بیاورید!
علاوه بر این، عناصری که در خروجی دریافت می‌کنید تبدیل یا تغییر شکل داده نمی‌شوند؛ آن‌ها دقیقاً همان عناصری هستند که در ورودی وجود داشتند.


📝 Where

Conventions-UsedThis-Book

📖 Where در LINQ


⚠️ محدودیت‌ها


📝 سینتکس کوئری

where bool-expression

🔧 پیاده‌سازی Enumerable.Where

نسخه داخلی Enumerable.Where (بدون بررسی null) معادل کدی شبیه زیر است:

public static IEnumerable<TSource> Where<TSource>(
    this IEnumerable<TSource> source,
    Func<TSource, bool> predicate)
{
    foreach (TSource element in source)
        if (predicate(element))
            yield return element;
}

📌 توضیح

Where عناصری از دنباله ورودی را برمی‌گرداند که شرط داده‌شده (predicate) را برآورده می‌کنند.


✨ مثال ساده

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };

IEnumerable<string> query = names.Where(name => name.EndsWith("y"));

// خروجی:
// Harry
// Mary
// Jay

🔹 معادل در Query Syntax:

IEnumerable<string> query =
    from n in names
    where n.EndsWith("y")
    select n;

🌀 چند شرط Where در یک کوئری

یک عبارت where می‌تواند چند بار در کوئری ظاهر شود و با let, orderby, یا join ترکیب شود:

from n in names
where n.Length > 3
let u = n.ToUpper()
where u.EndsWith("Y")
select u;

// خروجی:
// HARRY
// MARY

🔸 قوانین scoping استاندارد #C اعمال می‌شوند. یعنی نمی‌توانید قبل از تعریف یک متغیر (با range variable یا let) به آن ارجاع دهید.


🔢 Indexed Filtering (فیلترگذاری بر اساس ایندکس)

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

IEnumerable<string> query = names.Where((n, i) => i % 2 == 0);

// خروجی:
// Tom
// Harry
// Jay

⚠️ در EF Core استفاده از این قابلیت باعث Exception می‌شود.


🔍 مقایسه با LIKE در EF Core

متدهای زیر در رشته‌ها به SQL LIKE ترجمه می‌شوند:

مثال:

c.Name.Contains("abc")

به SQL معادل زیر تبدیل می‌شود:

customer.Name LIKE '%abc%'

(در واقع نسخه پارامتری‌شده ساخته می‌شود، نه رشته مستقیم.)

🔹 برای مقایسه با ستون دیگر باید از متد EF.Functions.Like استفاده کنید:

where EF.Functions.Like(c.Description, "%" + c.Name + "%")

این متد امکان مقایسه‌های پیچیده‌تر را هم می‌دهد، مثل:

LIKE 'abc%def%'

🔠 مقایسه رشته‌ای با < و > در EF Core

برای مقایسه ترتیبی رشته‌ها از متد string.CompareTo استفاده کنید:

dbContext.Purchases
    .Where(p => p.Description.CompareTo("C") < 0);

📌 این کد به عملگرهای < و > در SQL نگاشت می‌شود.


🗂 استفاده از IN در EF Core

در EF Core می‌توانید Contains را روی یک مجموعه محلی استفاده کنید:

string[] chosenOnes = { "Tom", "Jay" };

from c in dbContext.Customers
where chosenOnes.Contains(c.Name)
select c;

معادل SQL:

WHERE customer.Name IN ("Tom", "Jay")

⚠️ اگر مجموعه محلی آرایه‌ای از entity یا نوع غیر scalar باشد، EF Core ممکن است به‌جای آن EXISTS تولید کند.


⏩ عملگرهای بعدی

Conventions-UsedThis-Book

📖 Take و Skip در LINQ


📝 توضیح کلی

این دو متد معمولاً با هم استفاده می‌شن، مخصوصاً وقتی می‌خوایم صفحه‌بندی (Paging) در یک اپلیکیشن وب رو پیاده‌سازی کنیم.


🌐 مثال کاربردی (Paging در EF Core)

فرض کن کاربر توی دیتابیس کتاب‌ها دنبال عبارت "mercury" می‌گرده و ۱۰۰ نتیجه پیدا می‌شه.

📌 برای گرفتن ۲۰ نتیجه اول:

IQueryable<Book> query = dbContext.Books
    .Where(b => b.Title.Contains("mercury"))
    .OrderBy(b => b.Title)
    .Take(20);

📌 برای گرفتن کتاب‌های شماره ۲۱ تا ۴۰:

IQueryable<Book> query = dbContext.Books
    .Where(b => b.Title.Contains("mercury"))
    .OrderBy(b => b.Title)
    .Skip(20)
    .Take(20);

⚙️ نحوه ترجمه در SQL

در EF Core:


🔄 متدهای TakeLast و SkipLast


🚀 قابلیت جدید از .NET 6

از نسخه .NET 6، متد Take یک نسخه overload جدید داره که متغیر Range رو قبول می‌کنه. این نسخه می‌تونه جایگزین تمام چهار متد بشه.

📌 مثال‌ها:

Take(5..)
// معادل Skip(5)

Take(..^5)
// معادل SkipLast(5)

یعنی می‌تونی خیلی تمیزتر و کوتاه‌تر کد بزنی ✨


⏩ عملگرهای بعدی

Conventions-UsedThis-Book

🔹 TakeWhile و SkipWhile


⚙️ TakeWhile

TakeWhile عناصر دنباله ورودی را به ترتیب پیمایش می‌کند و هر عنصر را تا زمانی که شرط داده‌شده true باشد برمی‌گرداند.
به محض اینکه شرط false شود، بقیه عناصر نادیده گرفته می‌شوند.

int[] numbers = { 3, 5, 2, 234, 4, 1 };
var takeWhileSmall = numbers.TakeWhile(n => n < 100); // خروجی: { 3, 5, 2 }

⚙️ SkipWhile

SkipWhile هم دنباله ورودی را پیمایش می‌کند، ولی عناصر را تا زمانی که شرط true باشد نادیده می‌گیرد.
بعد از اولین عنصری که شرط false شد، بقیه عناصر برگردانده می‌شوند.

int[] numbers = { 3, 5, 2, 234, 4, 1 };
var skipWhileSmall = numbers.SkipWhile(n => n < 100); // خروجی: { 234, 4, 1 }

⚠️ توجه:
TakeWhile و SkipWhile هیچ معادل SQL ندارند و در کوئری‌های EF Core استفاده از آن‌ها باعث Exception می‌شود.


🔹 Distinct و DistinctBy


✅ Distinct

Distinct دنباله ورودی را بدون تکراری‌ها برمی‌گرداند.
می‌توانید custom equality comparer هم به آن بدهید.

char[] distinctLetters = "HelloWorld".Distinct().ToArray();
string s = new string(distinctLetters); // خروجی: "HeloWrd"

می‌توانیم مستقیماً متدهای LINQ را روی string صدا بزنیم، چون string پیاده‌سازی‌کننده IEnumerable<char> است.


✅ DistinctBy

مثال:

new[] { 1.0, 1.1, 2.0, 2.1, 3.0, 3.1 }
    .DistinctBy(n => Math.Round(n, 0)); // خروجی: { 1, 2, 3 }

Conventions-UsedThis-Book

🎨 Select و SelectMany در LINQ


⚙️ توضیح کلی

وقتی روی پایگاه داده کوئری می‌زنیم، Select و SelectMany انعطاف‌پذیرترین ابزارها برای انجام join هستند.
اما برای کوئری‌های محلی (Local queries)، Join و GroupJoin کارآمدترین و سریع‌ترین ابزارها برای join محسوب می‌شوند.

Conventions-UsedThis-Book

Select

Conventions-UsedThis-Book

⚠️ محدودیت EF Core


📝 سینتکس کوئری

select projection-expression

🔧 پیاده‌سازی Enumerable

نسخه داخلی Enumerable.Select به شکل زیر است:

public static IEnumerable<TResult> Select<TSource,TResult>(
    this IEnumerable<TSource> source,
    Func<TSource,TResult> selector)
{
    foreach (TSource element in source)
        yield return selector(element);
}

🔹 توضیح کلی


🔹 مثال پایه‌ای: گرفتن نام فونت‌ها

IEnumerable<string> query = from f in FontFamily.Families
                            select f.Name;

foreach (string name in query) 
    Console.WriteLine(name);

🔹 معادل Lambda Syntax:

IEnumerable<string> query = FontFamily.Families.Select(f => f.Name);

🔹 پروژه کردن به انواع ناشناس (Anonymous Types)

var query = from f in FontFamily.Families
            select new { f.Name, LineSpacing = f.GetLineSpacing(FontStyle.Bold) };
IEnumerable<FontFamily> query =
    from f in FontFamily.Families
    where f.IsStyleAvailable(FontStyle.Strikeout)
    select f;

foreach (FontFamily ff in query)
    Console.WriteLine(ff.Name);

در این موارد، کامپایلر هنگام تبدیل به Fluent Syntax، projection را حذف می‌کند.


🔹 Indexed Projection

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };
IEnumerable<string> query = names.Select((s, i) => i + "=" + s);
// خروجی: { "0=Tom", "1=Dick", "2=Harry", "3=Mary", "4=Jay" }

🔹 Subqueries و Object Hierarchies

string tempPath = Path.GetTempPath();
DirectoryInfo[] dirs = new DirectoryInfo(tempPath).GetDirectories();

var query = from d in dirs
            where (d.Attributes & FileAttributes.System) == 0
            select new
            {
                DirectoryName = d.FullName,
                Created = d.CreationTime,
                Files = from f in d.GetFiles()
                        where (f.Attributes & FileAttributes.Hidden) == 0
                        select new { FileName = f.Name, f.Length }
            };

foreach (var dirFiles in query)
{
    Console.WriteLine("Directory: " + dirFiles.DirectoryName);
    foreach (var file in dirFiles.Files)
        Console.WriteLine("  " + file.FileName + " Len: " + file.Length);
}

🔹 Deferred Execution در Local Queries

🌀 Subqueries و Joins در EF Core


🔹 Subquery Projections در EF Core

var query =
    from c in dbContext.Customers
    select new {
        c.Name,
        Purchases = (
            from p in dbContext.Purchases
            where p.CustomerID == c.ID && p.Price > 1000
            select new { p.Description, p.Price }
        ).ToList()
    };

foreach (var namePurchases in query)
{
    Console.WriteLine("Customer: " + namePurchases.Name);
    foreach (var purchaseDetail in namePurchases.Purchases)
        Console.WriteLine("  - $$$: " + purchaseDetail.Price);
}

⚠️ دقت کنید که استفاده از ToList در subquery ضروری است، زیرا EF Core 3 نمی‌تواند queryable بسازد اگر subquery مستقیماً به DbContext ارجاع دهد. این محدودیت ممکن است در نسخه‌های بعدی EF Core برطرف شود.


🔹 مزیت این سبک


🔹 نگاشت داده‌های سلسله‌مراتبی


🔹 استفاده از Navigation Property

مثال ساده‌تر با استفاده از Navigation Property Purchases در Customer:

from c in dbContext.Customers
select new
{
    c.Name,
    Purchases = from p in c.Purchases  // Purchases نوع List<Purchase> است
                where p.Price > 1000
                select new { p.Description, p.Price }
};

در EF Core 3، هنگام استفاده از Navigation Property نیازی به ToList نیست.


🔹 شبیه‌سازی Inner Join

from c in dbContext.Customers
where c.Purchases.Any(p => p.Price > 1000)
select new {
    c.Name,
    Purchases = from p in c.Purchases
                where p.Price > 1000
                select new { p.Description, p.Price }
};
from c in dbContext.Customers
let highValueP = from p in c.Purchases
                 where p.Price > 1000
                 select new { p.Description, p.Price }
where highValueP.Any()
select new { c.Name, Purchases = highValueP };
where highValueP.Count() >= 2
select new { c.Name, Purchases = highValueP };

🔹 Projection به Types مشخص

IQueryable<CustomerEntity> query =
    from c in dbContext.Customers
    select new CustomerEntity
    {
        Name = c.Name,
        Purchases = (
            from p in c.Purchases
            where p.Price > 1000
            select new PurchaseEntity
            {
                Description = p.Description,
                Value = p.Price
            }
        ).ToList()
    };

// اجرای کوئری و تبدیل خروجی به List
List<CustomerEntity> result = query.ToList();

کلاس‌های DTO معمولاً هیچ منطق تجاری ندارند و صرفاً برای انتقال داده بین لایه‌ها یا سیستم‌ها استفاده می‌شوند.


🔹 نکته کلیدی

Conventions-UsedThis-Book

🌊 SelectMany در LINQ

Conventions-UsedThis-Book

🌊 SelectMany در LINQ – جزئیات و مثال‌ها


🔹 Query Syntax

from identifier1 in enumerable-expression1
from identifier2 in enumerable-expression2
...

🔹 Enumerable Implementation

public static IEnumerable<TResult> SelectMany<TSource,TResult>
    (IEnumerable<TSource> source,
     Func<TSource,IEnumerable<TResult>> selector)
{
    foreach (TSource element in source)
        foreach (TResult subElement in selector(element))
            yield return subElement;
}

🔹 مثال ساده: flatten کردن کلمات از fullNames

string[] fullNames = { "Anne Williams", "John Fred Smith", "Sue Green" };

IEnumerable<string> query = fullNames.SelectMany(name => name.Split());
foreach (string name in query)
    Console.Write(name + "|");  
// خروجی: Anne|Williams|John|Fred|Smith|Sue|Green|
IEnumerable<string[]> query = fullNames.Select(name => name.Split());
foreach (string[] stringArray in query)
    foreach (string name in stringArray)
        Console.Write(name + "|");

🔹 Query Syntax و چند متغیره بودن

IEnumerable<string> query =
    from fullName in fullNames
    from name in fullName.Split()   // ترجمه به SelectMany
    select name;
IEnumerable<string> query =
    from fullName in fullNames
    from name in fullName.Split()
    select name + " came from " + fullName;
Anne came from Anne Williams
Williams came from Anne Williams
John came from John Fred Smith
...

🔹 مشکل در Fluent Syntax

from fullName in fullNames
from x in fullName.Split().Select(name => new { name, fullName })
orderby x.fullName, x.name
select x.name + " came from " + x.fullName;
IEnumerable<string> query = fullNames
    .SelectMany(fName => fName.Split()
        .Select(name => new { name, fName }))
    .OrderBy(x => x.fName)
    .ThenBy(x => x.name)
    .Select(x => x.name + " came from " + x.fName);

🤔 فکر کردن به سبک Query Syntax در LINQ


🔹 چرا query syntax مفید است؟


1️⃣ گسترش و flatten کردن subsequenceها

from fullName in fullNames
from name in fullName.Split()
IEnumerable<string> query = 
    from c in dbContext.Customers
    from p in c.Purchases
    select c.Name + " bought a " + p.Description;
Tom bought a Bike
Tom bought a Holiday
Dick bought a Phone
Harry bought a Car
...

2️⃣ تولید Cartesian Product یا Cross Join

int[] numbers = { 1, 2, 3 };
string[] letters = { "a", "b" };

IEnumerable<string> query = 
    from n in numbers
    from l in letters
    select n.ToString() + l;
// خروجی: { "1a", "1b", "2a", "2b", "3a", "3b" }

🔹 Join کردن با SelectMany

string[] players = { "Tom", "Jay", "Mary" };

IEnumerable<string> query = 
    from name1 in players
    from name2 in players
    where name1.CompareTo(name2) < 0
    orderby name1, name2
    select name1 + " vs " + name2;

// خروجی: { "Jay vs Mary", "Jay vs Tom", "Mary vs Tom" }

🔹 SelectMany در EF Core

مثال Cross Join

var query = 
    from c in dbContext.Customers
    from p in dbContext.Purchases
    select c.Name + " might have bought a " + p.Description;

مثال Equi-Join (SQL-style)

var query = 
    from c in dbContext.Customers
    from p in dbContext.Purchases
    where c.ID == p.CustomerID
    select c.Name + " bought a " + p.Description;

🔹 استفاده از Collection Navigation Properties

from c in dbContext.Customers
from p in c.Purchases
select new { c.Name, p.Description };

🔹 اضافه کردن فیلترها

from c in dbContext.Customers
where c.Name.StartsWith("T")
from p in c.Purchases
select new { c.Name, p.Description };

🔹 اضافه کردن جداول فرزند

from c in dbContext.Customers
from p in c.Purchases
from pi in p.PurchaseItems
select new { c.Name, p.Description, pi.Detail };

🔹 استفاده از Navigation Property والد

from c in dbContext.Customers
select new { Name = c.Name, SalesPerson = c.SalesPerson.Name };

↔️ Outer Joins با SelectMany در LINQ و EF Core


🔹 مثال اولیه با Subquery

from c in dbContext.Customers
select new {
    c.Name,
    Purchases = from p in c.Purchases
                where p.Price > 1000
                select new { p.Description, p.Price }
};

🔹 مشکل وقتی SelectMany استفاده شود

from c in dbContext.Customers
from p in c.Purchases
where p.Price > 1000
select new { c.Name, p.Description, p.Price };

🔹 راه حل برای Left Outer Join تخت

from c in dbContext.Customers
from p in c.Purchases.DefaultIfEmpty()
select new { c.Name, p.Description, Price = (decimal?)p.Price };

🔹 نسخه مقاوم (Robust)

from c in dbContext.Customers
from p in c.Purchases.DefaultIfEmpty()
select new {
    c.Name,
    Descript = p == null ? null : p.Description,
    Price = p == null ? (decimal?) null : p.Price
};

🔹 اعمال فیلتر قیمت

from c in dbContext.Customers
from p in c.Purchases.Where(p => p.Price > 1000).DefaultIfEmpty()
select new {
    c.Name,
    Descript = p == null ? null : p.Description,
    Price = p == null ? (decimal?) null : p.Price
};

اگر به نوشتن outer join در SQL عادت داری، ممکنه وسوسه بشی که گزینه‌ی ساده‌تر یعنی Select subquery رو نادیده بگیری و به سمت روش تخت و پیچیده‌ی SQL-centric بری که آشناتر به نظر می‌رسه.

✅ واقعیت اینه که hierarchical result set که از یک Select subquery به دست میاد، اغلب برای queryهای سبک outer join بهتره، چون نیازی به مدیریت nullهای اضافی نداری و کار تمیزتر انجام می‌شه.

Joining

Conventions-UsedThis-Book

✨ نحوۀ Query در LINQ

from outer-var in outer-enumerable
join inner-var in inner-enumerable on outer-key-expr equals inner-key-expr
[ into identifier ]

📖 مرور کلی (Overview)

🔹 Join و GroupJoin دو توالی ورودی (input sequences) را به یک توالی خروجی (output sequence) ترکیب می‌کنند.

Join و GroupJoin یک راهبرد جایگزین برای Select و SelectMany ارائه می‌دهند.

مزیت Join و GroupJoin این است که آن‌ها به‌شکل کارآمد روی مجموعه‌های محلی (local in-memory collections) اجرا می‌شوند، چون ابتدا توالی درونی (inner sequence) را داخل یک lookup کلیددار (keyed lookup) بارگذاری می‌کنند و به این ترتیب از نیاز به پیمایش (enumerate) مکرر روی هر عنصر داخلی جلوگیری می‌کنند.

⚠️ عیب آن‌ها این است که تنها معادل inner join و left outer join را ارائه می‌دهند؛ برای cross join و non-equi join همچنان باید از Select/SelectMany استفاده کرد.

📌 در کوئری‌های EF Core، استفاده از Join و GroupJoin مزیت خاصی نسبت به Select و SelectMany ندارد.

📊 جدول ۹-۱ تفاوت‌های میان هر یک از راهبردهای join را خلاصه می‌کند.

Conventions-UsedThis-Book

🔗 Join

اپراتور Join یک inner join انجام می‌دهد و یک توالی خروجی مسطح (flat output sequence) تولید می‌کند.

🔹 مثال زیر، همۀ مشتریان (customers) را همراه با خریدهایشان (purchases) فهرست می‌کند، بدون اینکه از ویژگی ناوبری (navigation property) استفاده شود:

IQueryable<string> query =
  from c in dbContext.Customers
  join p in dbContext.Purchases on c.ID equals p.CustomerID
  select c.Name + " bought a " + p.Description;

📋 نتایج دقیقاً همان چیزی است که با یک کوئری به سبک SelectMany به دست می‌آید:

Tom bought a Bike
Tom bought a Holiday
Dick bought a Phone
Harry bought a Car

⚡ مزیت Join در برابر SelectMany

برای دیدن مزیت Join در مقایسه با SelectMany، باید کوئری را به حالت محلی (local query) تبدیل کنیم.

اول، تمام مشتریان و خریدها را در آرایه‌ها کپی می‌کنیم و سپس روی آرایه‌ها کوئری می‌زنیم:

Customer[] customers = dbContext.Customers.ToArray();
Purchase[] purchases = dbContext.Purchases.ToArray();

var slowQuery = from c in customers
                from p in purchases
                where c.ID == p.CustomerID
                select c.Name + " bought a " + p.Description;

var fastQuery = from c in customers
                join p in purchases on c.ID equals p.CustomerID
                select c.Name + " bought a " + p.Description;

هر دو کوئری نتیجه یکسانی برمی‌گردانند، اما کوئری با Join به‌مراتب سریع‌تر است. دلیلش این است که پیاده‌سازی در Enumerable، مجموعه داخلی (purchases) را ابتدا به‌صورت یک keyed lookup بارگذاری می‌کند.


📝 نحوۀ کلی Join

نحوۀ نوشتن join به‌طور کلی به شکل زیر است:

join inner-var in inner-sequence on outer-key-expr equals inner-key-expr

اپراتورهای Join در LINQ بین توالی بیرونی (outer sequence) و توالی درونی (inner sequence) تمایز قائل می‌شوند.

📌 Join فقط inner join انجام می‌دهد؛ یعنی مشتریانی که خریدی ندارند از خروجی حذف می‌شوند.
در inner join می‌توانید توالی بیرونی و درونی را با هم جابه‌جا کنید و همچنان نتیجه یکسانی بگیرید:

from p in purchases                                // p حالا outer است
join c in customers on p.CustomerID equals c.ID    // c حالا inner است
...

🧩 چندین Join در یک کوئری

شما می‌توانید چندین عبارت join در یک کوئری اضافه کنید.
مثلاً اگر هر خرید (purchase) یک یا چند آیتم خرید (purchase items) داشته باشد:

from c in customers
join p in purchases on c.ID equals p.CustomerID           // first join
join pi in purchaseItems on p.ID equals pi.PurchaseID     // second join
...

📌 در اینجا، purchases در اولین join به‌عنوان inner sequence عمل می‌کند و در دومین join به‌عنوان outer sequence.

معادل ناکارآمد همین کار با foreach به شکل زیر است:

foreach (Customer c in customers)
  foreach (Purchase p in purchases)
    if (c.ID == p.CustomerID)
      foreach (PurchaseItem pi in purchaseItems)
        if (p.ID == pi.PurchaseID)
          Console.WriteLine (c.Name + "," + p.Price + "," + pi.Detail);

در نحوۀ Query، متغیرهای joinهای قبلی همچنان در دسترس هستند—دقیقاً مثل کاری که در کوئری‌های به سبک SelectMany اتفاق می‌افتد.
همچنین می‌توانید بین joinها، از where و let استفاده کنید.


🔑 Join با چند کلید

می‌توانید روی چند کلید به‌طور همزمان join انجام دهید. برای این کار از anonymous types استفاده می‌شود:

from x in sequenceX
join y in sequenceY on new { K1 = x.Prop1, K2 = x.Prop2 }
                   equals new { K1 = y.Prop3, K2 = y.Prop4 }
...

برای اینکه این کار درست انجام شود، دو anonymous type باید دقیقاً یک ساختار (structure) داشته باشند.
کامپایلر هر دو را با یک نوع داخلی یکسان پیاده‌سازی می‌کند، بنابراین کلیدهای join با هم سازگار می‌شوند.

🔗 Join در Fluent Syntax

🔹 کوئری زیر در نحوۀ Query:

from c in customers
join p in purchases on c.ID equals p.CustomerID
select new { c.Name, p.Description, p.Price };

به شکل Fluent Syntax این‌طور نوشته می‌شود:

customers.Join(                // outer collection
    purchases,                 // inner collection
    c => c.ID,                 // outer key selector
    p => p.CustomerID,         // inner key selector
    (c, p) => new              // result selector
        { c.Name, p.Description, p.Price }
);

📌 عبارت result selector در انتها، هر عنصر خروجی را می‌سازد.


📑 افزودن عبارات دیگر (orderby و …)

اگر قبل از بخش select عباراتی مثل orderby داشته باشیم:

from c in customers
join p in purchases on c.ID equals p.CustomerID
orderby p.Price
select c.Name + " bought a " + p.Description;

در Fluent Syntax باید یک نوع ناشناس موقت (temporary anonymous type) بسازیم تا هر دو متغیر c و p پس از join در دسترس باشند:

customers.Join(                  // outer collection
    purchases,                   // inner collection
    c => c.ID,                   // outer key selector
    p => p.CustomerID,           // inner key selector
    (c, p) => new { c, p })      // result selector
    .OrderBy(x => x.p.Price)
    .Select(x => x.c.Name + " bought a " + x.p.Description);

✅ در عمل، نحوۀ Query برای join معمولاً ترجیح داده می‌شود، چون ساده‌تر و خواناتر است.


👥 GroupJoin

🔹 GroupJoin همان کار Join را انجام می‌دهد، اما به‌جای اینکه خروجی مسطح بدهد، یک خروجی سلسله‌مراتبی (hierarchical result) تولید می‌کند که بر اساس هر عنصر بیرونی (outer element) گروه‌بندی شده است.
همچنین امکان left outer join را فراهم می‌کند.
📌 توجه: GroupJoin در حال حاضر در EF Core پشتیبانی نمی‌شود.


✍️ نحوۀ Query برای GroupJoin

نحوۀ Query برای GroupJoin مثل Join است، اما با کلمۀ کلیدی into دنبال می‌شود.

🔹 یک مثال ساده با کوئری محلی:

Customer[] customers = dbContext.Customers.ToArray();
Purchase[] purchases = dbContext.Purchases.ToArray();

IEnumerable<IEnumerable<Purchase>> query =
  from c in customers
  join p in purchases on c.ID equals p.CustomerID
  into custPurchases
  select custPurchases;   // custPurchases یک توالی است

📌 عبارت into تنها زمانی به GroupJoin تبدیل می‌شود که بلافاصله بعد از یک join بیاید.
اگر بعد از select یا group بیاید، معنایش query continuation است.
هر دو مورد یک ویژگی مشترک دارند: معرفی یک متغیر جدید (range variable).

🔹 خروجی یک توالی از توالی‌ها است که می‌توانیم آن را این‌طور پیمایش کنیم:

foreach (IEnumerable<Purchase> purchaseSequence in query)
    foreach (Purchase p in purchaseSequence)
        Console.WriteLine(p.Description);

👤 استفاده کاربردی‌تر از GroupJoin

در حالت معمول، کوئری را این‌طور می‌نویسیم تا ارتباط مشتری با خریدهایش حفظ شود:

from c in customers
join p in purchases on c.ID equals p.CustomerID
into custPurchases
select new { CustName = c.Name, custPurchases };

این معادل است با این کوئری (که ناکارآمد است):

from c in customers
select new
{
    CustName = c.Name,
    custPurchases = purchases.Where(p => c.ID == p.CustomerID)
};

🔄 Left Outer Join در GroupJoin

به‌طور پیش‌فرض، GroupJoin معادل یک left outer join است.
برای گرفتن inner join (حذف مشتریانی که خریدی ندارند)، باید روی custPurchases فیلتر بزنید:

from c in customers
join p in purchases on c.ID equals p.CustomerID
into custPurchases
where custPurchases.Any()
select ...

📌 عبارات بعد از group-join into روی زیرتوالی‌ها (subsequences) عمل می‌کنند، نه روی تک‌تک عناصر.
پس اگر بخواهید روی خریدهای منفرد فیلتر کنید، باید قبل از join از Where استفاده کنید:

from c in customers
join p in purchases.Where(p2 => p2.Price > 1000)
     on c.ID equals p.CustomerID
into custPurchases ...

همچنین می‌توانید کوئری‌های lambda با GroupJoin درست مثل Join بسازید.


🪄 Flat Outer Joins

گاهی می‌خواهید هم outer join داشته باشید و هم یک خروجی مسطح (flat result set).

📌 راه‌حل: اول GroupJoin، بعد DefaultIfEmpty روی هر زیرتوالی، و در نهایت SelectMany:

from c in customers
join p in purchases on c.ID equals p.CustomerID into custPurchases
from cp in custPurchases.DefaultIfEmpty()
select new
{
    CustName = c.Name,
    Price = cp == null ? (decimal?) null : cp.Price
};

✅ اگر زیرتوالی خریدها خالی باشد، DefaultIfEmpty یک توالی با مقدار null تولید می‌کند.
عبارت دوم from به SelectMany ترجمه می‌شود و همه زیرتوالی‌های خرید را گسترش داده و در یک توالی واحد از عناصر خرید مسطح می‌کند.

🔍 Joining with Lookups

اپراتورهای Join و GroupJoin در کلاس Enumerable در دو مرحله عمل می‌کنند:

  1. ابتدا توالی درونی (inner sequence) را داخل یک lookup بارگذاری می‌کنند.
  2. سپس توالی بیرونی (outer sequence) را در ترکیب با lookup پردازش می‌کنند.

📦 Lookup چیست؟

یک lookup در واقع مجموعه‌ای از گروه‌ها (groupings) است که می‌توان به‌طور مستقیم با کلید (key) به آن‌ها دسترسی داشت.
می‌توانید آن را مثل یک دیکشنری از توالی‌ها تصور کنید—یک دیکشنری که می‌تواند چندین عنصر را زیر یک کلید نگه دارد (گاهی به آن multidictionary می‌گویند).

📌 Lookup فقط خواندنی (read-only) است و رابط آن به شکل زیر تعریف می‌شود:

public interface ILookup<TKey, TElement> :
    IEnumerable<IGrouping<TKey, TElement>>, IEnumerable
{
    int Count { get; }
    bool Contains(TKey key);
    IEnumerable<TElement> this[TKey key] { get; }
}

⏳ اجرای Lazy

مثل سایر اپراتورهای LINQ که خروجی تولید می‌کنند، اپراتورهای join نیز Deferred Execution یا Lazy Execution دارند.
یعنی lookup ساخته نمی‌شود تا زمانی که پیمایش (enumeration) خروجی شروع شود—و در آن لحظه کل lookup یکجا ساخته می‌شود.


🛠 ساختن Lookup دستی

می‌توانید lookup را به‌طور دستی بسازید و کوئری بزنید. این کار چند مزیت دارد:

🔹 متد ToLookup یک lookup می‌سازد. مثال: بارگذاری تمام خریدها (purchases) در یک lookup که بر اساس CustomerID کلیدگذاری شده است:

ILookup<int?, Purchase> purchLookup =
    purchases.ToLookup(p => p.CustomerID, p => p);

📖 خواندن از Lookup

خواندن از یک lookup شبیه خواندن از یک دیکشنری است، با این تفاوت که Indexer یک توالی از آیتم‌های منطبق برمی‌گرداند (نه فقط یک آیتم).

foreach (Purchase p in purchLookup[1])
    Console.WriteLine(p.Description);

این کد تمام خریدهای مشتری با ID برابر 1 را نمایش می‌دهد.


⚡ کارایی Lookup مثل Join/GroupJoin

وقتی یک lookup داشته باشید، می‌توانید کوئری‌های SelectMany/Select بنویسید که به‌اندازۀ کوئری‌های Join/GroupJoin کارآمد هستند.

🔹 Join معادل استفاده از SelectMany روی یک lookup است:

from c in customers
from p in purchLookup[c.ID]
select new { c.Name, p.Description, p.Price };

📋 خروجی:

Tom Bike 500
Tom Holiday 2000
Dick Bike 600
Dick Phone 300
...

🪄 Outer Join با DefaultIfEmpty

اضافه‌کردن DefaultIfEmpty باعث می‌شود کوئری معادل یک outer join شود:

from c in customers
from p in purchLookup[c.ID].DefaultIfEmpty()
select new
{
    c.Name,
    Descript = p == null ? null : p.Description,
    Price = p == null ? (decimal?) null : p.Price
};

🧩 GroupJoin معادل Lookup

GroupJoin معادل این است که lookup را داخل projection بخوانیم:

from c in customers
select new
{
    CustName = c.Name,
    CustPurchases = purchLookup[c.ID]
};

⚙️ پیاده‌سازی Enumerable.Join

ساده‌ترین پیاده‌سازی معتبر Enumerable.Join (بدون درنظر گرفتن null-check):

public static IEnumerable<TResult> Join
    <TOuter, TInner, TKey, TResult>(
        this IEnumerable<TOuter> outer,
        IEnumerable<TInner> inner,
        Func<TOuter, TKey> outerKeySelector,
        Func<TInner, TKey> innerKeySelector,
        Func<TOuter, TInner, TResult> resultSelector)
{
    ILookup<TKey, TInner> lookup = inner.ToLookup(innerKeySelector);
    return
        from outerItem in outer
        from innerItem in lookup[outerKeySelector(outerItem)]
        select resultSelector(outerItem, innerItem);
}

⚙️ پیاده‌سازی Enumerable.GroupJoin

پیاده‌سازی GroupJoin شبیه Join است، اما ساده‌تر:

public static IEnumerable<TResult> GroupJoin
    <TOuter, TInner, TKey, TResult>(
        this IEnumerable<TOuter> outer,
        IEnumerable<TInner> inner,
        Func<TOuter, TKey> outerKeySelector,
        Func<TInner, TKey> innerKeySelector,
        Func<TOuter, IEnumerable<TInner>, TResult> resultSelector)
{
    ILookup<TKey, TInner> lookup = inner.ToLookup(innerKeySelector);
    return
        from outerItem in outer
        select resultSelector(
            outerItem,
            lookup[outerKeySelector(outerItem)]);
}

🔗 The Zip Operator

IEnumerable<TFirst>, IEnumerable<TSecond> → IEnumerable<TResult>

اپراتور Zip دو توالی را گام‌به‌گام (مثل زیپ) پیمایش می‌کند و با اعمال یک تابع روی هر جفت عنصر، یک توالی جدید می‌سازد.

🔹 مثال:

int[] numbers = { 3, 5, 7 };
string[] words = { "three", "five", "seven", "ignored" };

IEnumerable<string> zip =
    numbers.Zip(words, (n, w) => n + "=" + w);

📋 خروجی:

3=three
5=five
7=seven

📌 عناصر اضافه در هر یک از توالی‌ها نادیده گرفته می‌شوند.
⚠️ Zip در EF Core پشتیبانی نمی‌شود.

📑 مرتب‌سازی (Ordering)

IEnumerable<TSource> → IOrderedEnumerable<TSource>

Conventions-UsedThis-Book

عملگرهای مرتب‌سازی (Ordering operators) همان عناصر را بازمی‌گردانند، اما در ترتیب متفاوت.

🔀 OrderBy, OrderByDescending, ThenBy, ThenByDescending

📌 آرگومان‌های OrderBy و OrderByDescending

Conventions-UsedThis-Book

نوع بازگشتی = IOrderedEnumerable<TSource>

🔹 آرگومان‌های ThenBy و ThenByDescending

Conventions-UsedThis-Book

📑 نحوۀ Query (Query syntax)

orderby expression1 [descending] [, expression2 [descending] ... ]

📖 مرور کلی (Overview)

IEnumerable<string> query = names.OrderBy(s => s);
IEnumerable<string> query = names.OrderBy(s => s.Length);
// نتیجه: { "Jay", "Tom", "Mary", "Dick", "Harry" };
IEnumerable<string> query = names.OrderBy(s => s.Length).ThenBy(s => s);
// نتیجه: { "Jay", "Tom", "Dick", "Mary", "Harry" };
names.OrderBy(s => s.Length).ThenBy(s => s[1]).ThenBy(s => s[0]);

🔄 معادل در نحوۀ Query

from s in names
orderby s.Length, s[1], s[0]
select s;

⚠️ نمونه اشتباه: این در واقع ابتدا بر اساس s[1] و سپس s.Length مرتب می‌کند (یا در کوئری پایگاه داده فقط بر اساس s[1] مرتب می‌کند و ترتیب قبلی را نادیده می‌گیرد):

from s in names
orderby s.Length
orderby s[1]
...

🔽 OrderByDescending و ThenByDescending

این اپراتورها همان کارهای قبلی را انجام می‌دهند اما خروجی را به ترتیب معکوس می‌دهند.

مثال EF Core: بازیابی خریدها بر اساس قیمت نزولی و در صورت برابر بودن قیمت، به ترتیب الفبایی:

dbContext.Purchases
    .OrderByDescending(p => p.Price)
    .ThenBy(p => p.Description);

معادل در نحوۀ Query:

from p in dbContext.Purchases
orderby p.Price descending, p.Description
select p;

📚 Comparers و Collations

names.OrderBy(n => n, StringComparer.CurrentCultureIgnoreCase);
from p in dbContext.Purchases
orderby p.Description.ToUpper()
select p;

🔹 IOrderedEnumerable و IOrderedQueryable

🔹 مثال: ساخت کوئری مرحله‌ای

IOrderedEnumerable<string> query1 = names.OrderBy(s => s.Length);
IOrderedEnumerable<string> query2 = query1.ThenBy(s => s);

⚠️ اگر query1 از نوع IEnumerable<string> تعریف شود، خط دوم کامپایل نمی‌شود—چون ThenBy به ورودی از نوع IOrderedEnumerable<string> نیاز دارد.


🔹 استفاده از تایپ ضمنی (Implicit Typing)

var query1 = names.OrderBy(s => s.Length);
var query2 = query1.ThenBy(s => s);
var query = names.OrderBy(s => s.Length);
query = query.Where(n => n.Length > 3);  // خطای زمان کامپایل

✅ راه‌حل‌ها:

  1. استفاده از تایپ صریح
  2. یا فراخوانی AsEnumerable() بعد از OrderBy:
var query = names.OrderBy(s => s.Length).AsEnumerable();
query = query.Where(n => n.Length > 3);  // درست

Grouping

Conventions-UsedThis-Book

📚 GroupBy

IEnumerable<TSource> → IEnumerable<IGrouping<TKey, TElement>>

Conventions-UsedThis-Book

📑 GroupBy

IEnumerable<TSource> → IEnumerable<IGrouping<TKey, TElement>>

🔍 نحوۀ Query (Query syntax)

group element-expression by key-expression

📖 مرور کلی (Overview)

string[] files = Directory.GetFiles(Path.GetTempPath());

IEnumerable<IGrouping<string, string>> query =
    files.GroupBy(file => Path.GetExtension(file));
var query = files.GroupBy(file => Path.GetExtension(file));

🔹 پیمایش نتایج

foreach (IGrouping<string, string> grouping in query)
{
    Console.WriteLine("Extension: " + grouping.Key);
    foreach (string filename in grouping)
        Console.WriteLine("   - " + filename);
}

📋 خروجی نمونه:

Extension: .pdf
  -- chapter03.pdf
  -- chapter04.pdf
Extension: .doc
  -- todo.doc
  -- menu.doc
  -- Copy of menu.doc

🛠 پیاده‌سازی داخلی

public interface IGrouping<TKey, TElement> : IEnumerable<TElement>, IEnumerable
{
    TKey Key { get; }    // کلید اعمال شده روی زیرتوالی به‌صورت کلی
}
files.GroupBy(file => Path.GetExtension(file), file => file.ToUpper());

📋 خروجی نمونه:

Extension: .pdf
  -- CHAPTER03.PDF
  -- CHAPTER04.PDF
Extension: .doc
  -- TODO.DOC

⚠️ نکات مهم

files.GroupBy(file => Path.GetExtension(file), file => file.ToUpper())
     .OrderBy(grouping => grouping.Key);

🔹 معادل در نحوۀ Query

group element-expr by key-expr

مثال:

from file in files
group file.ToUpper() by Path.GetExtension(file);
from file in files
group file.ToUpper() by Path.GetExtension(file) into grouping
orderby grouping.Key
select grouping;

🔹 ادامه‌ی کوئری‌ها (Query Continuations)

from file in files
group file.ToUpper() by Path.GetExtension(file) into grouping
where grouping.Count() >= 5
select grouping;

🔹 مثال Aggregation

string[] votes = { "Dogs", "Cats", "Cats", "Dogs", "Dogs" };

IEnumerable<string> query = from vote in votes
                            group vote by vote into g
                            orderby g.Count() descending
                            select g.Key;

string winner = query.First();    // Dogs

📑 GroupBy در EF Core

مثال: انتخاب مشتریانی که حداقل دو خرید داشته‌اند بدون نیاز به گروه‌بندی:

from c in dbContext.Customers
where c.Purchases.Count >= 2
select c.Name + " has made " + c.Purchases.Count + " purchases";
from p in dbContext.Purchases
group p.Price by p.Date.Year into salesByYear
select new {
    Year       = salesByYear.Key,
    TotalValue = salesByYear.Sum()
};
from p in dbContext.Purchases
group p by p.Date.Year

⚠️ این روش در EF Core کار نمی‌کند.
راه‌حل ساده: قبل از گروه‌بندی .AsEnumerable() فراخوانی کنید تا گروه‌بندی روی کلاینت انجام شود.


🔹 گروه‌بندی با چند کلید

from n in names
group n by new { FirstLetter = n[0], Length = n.Length };

🔹 مقایسه‌کننده‌های سفارشی (Custom equality comparers)

group n by n.ToUpper()

📑 Chunk

IEnumerable<TSource> → IEnumerable<TElement[]>

Conventions-UsedThis-Book

📦 Chunk

foreach (int[] chunk in new[] { 1, 2, 3, 4, 5, 6, 7, 8 }.Chunk(3))
    Console.WriteLine(string.Join(", ", chunk));

خروجی:

1, 2, 3
4, 5, 6
7, 8

🔗 Set Operators

IEnumerable<TSource>, IEnumerable<TSource> → IEnumerable<TSource>

Conventions-UsedThis-Book

🔗 Concat, Union, UnionBy

int[] seq1 = { 1, 2, 3 }, seq2 = { 3, 4, 5 };

IEnumerable<int>
    concat = seq1.Concat(seq2),   // { 1, 2, 3, 3, 4, 5 }
    union  = seq1.Union(seq2);   // { 1, 2, 3, 4, 5 }
MethodInfo[] methods = typeof(string).GetMethods();
PropertyInfo[] props = typeof(string).GetProperties();
IEnumerable<MemberInfo> both = methods.Concat<MemberInfo>(props);
var methods = typeof(string).GetMethods().Where(m => !m.IsSpecialName);
var props   = typeof(string).GetProperties();
var both    = methods.Concat<MemberInfo>(props);
string[] seq1 = { "A", "b", "C" };
string[] seq2 = { "a", "B", "c" };

var union = seq1.UnionBy(seq2, x => x.ToUpperInvariant());
// union is { "A", "b", "C" }
var union = seq1.Union(seq2, StringComparer.InvariantCultureIgnoreCase);

🔹 Intersect, IntersectBy, Except, ExceptBy

int[] seq1 = { 1, 2, 3 }, seq2 = { 3, 4, 5 };

IEnumerable<int>
    commonality  = seq1.Intersect(seq2),    // { 3 }
    difference1  = seq1.Except(seq2),      // { 1, 2 }
    difference2  = seq2.Except(seq1);      // { 4, 5 }
SELECT number FROM numbers1Table
WHERE number NOT IN (SELECT number FROM numbers2Table)

🔹 Conversion Methods

Conventions-UsedThis-Book

🔄 OfType و Cast

ArrayList classicList = new ArrayList(); // در System.Collections
classicList.AddRange(new int[] { 3, 4, 5 });

IEnumerable<int> sequence1 = classicList.Cast<int>();

ادامه مثال بالا:

DateTime offender = DateTime.Now;
classicList.Add(offender);

IEnumerable<int>
    sequence2 = classicList.OfType<int>(), // OK - عنصر DateTime نادیده گرفته می‌شود
    sequence3 = classicList.Cast<int>();   // استثناء می‌دهد

پیاده‌سازی داخلی OfType:

public static IEnumerable<TSource> OfType<TSource>(IEnumerable source)
{
    foreach (object element in source)
        if (element is TSource)
            yield return (TSource)element;
}

پیاده‌سازی Cast مشابه است ولی تست سازگاری نوع را انجام نمی‌دهد:

public static IEnumerable<TSource> Cast<TSource>(IEnumerable source)
{
    foreach (object element in source)
        yield return (TSource)element;
}

مثال:

int[] integers = { 1, 2, 3 };

IEnumerable<long> test1 = integers.OfType<long>(); // صفر عنصر
IEnumerable<long> test2 = integers.Cast<long>();   // استثناء می‌دهد

راه‌حل: استفاده از Select:

IEnumerable<long> castLong = integers.Select(s => (long)s);
from TreeNode node in myTreeView.Nodes
...

🟢 ToArray, ToList, ToDictionary, ToHashSet, ToLookup

Conventions-UsedThis-Book

🟡 ToDictionary و ToLookup


🔹 AsEnumerable و AsQueryable


🔹 Element Operators

IEnumerable<TSource> → TSource

Conventions-UsedThis-Book

⚡ Methods ending in “OrDefault”


🔹 First, Last, and Single

Conventions-UsedThis-Book

🔹 First و Last

مثال زیر First و Last را نشان می‌دهد:

int[] numbers  = { 1, 2, 3, 4, 5 };
int first      = numbers.First();                     // 1
int last       = numbers.Last();                      // 5
int firstEven  = numbers.First(n => n % 2 == 0);     // 2
int lastEven   = numbers.Last(n => n % 2 == 0);      // 4

مثال First در مقابل FirstOrDefault:

int firstBigError  = numbers.First(n => n > 10);      // Exception
int firstBigNumber = numbers.FirstOrDefault(n => n > 10); // 0

🔹 Single و SingleOrDefault

مثال‌ها:

int onlyDivBy3 = numbers.Single(n => n % 3 == 0);      // 3
int divBy2Err  = numbers.Single(n => n % 2 == 0);      // خطا: 2 و 4 مطابقت دارند
int singleError = numbers.Single(n => n > 10);         // خطا
int noMatches   = numbers.SingleOrDefault(n => n > 10); // 0
int divBy2Error = numbers.SingleOrDefault(n => n % 2 == 0); // خطا
Customer cust = dataContext.Customers.Single(c => c.ID == 3);

🔹 ElementAt

Conventions-UsedThis-Book

🔹 ElementAt و ElementAtOrDefault

int[] numbers  = { 1, 2, 3, 4, 5 };
int third      = numbers.ElementAt(2);          // 3
int tenthError = numbers.ElementAt(9);          // Exception
int tenth      = numbers.ElementAtOrDefault(9); // 0

🔹 MinBy و MaxBy

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };
Console.WriteLine(names.MaxBy(n => n.Length));   // Harry
Console.WriteLine(names.Max(n => n.Length));    // 5
Console.WriteLine(names.MinBy(n => n.Length));  // Tom

🔹 DefaultIfEmpty


🔹 Aggregation Methods

IEnumerable<TSource> → scalar

Conventions-UsedThis-Book

🔹 Count و LongCount

Conventions-UsedThis-Book

int fullCount = new int[] { 5, 6, 7 }.Count();   // 3
int digitCount = "pa55w0rd".Count(c => char.IsDigit(c));   // 3

🔹 Min و Max

Conventions-UsedThis-Book

int[] numbers = { 28, 32, 14 };
int smallest = numbers.Min();  // 14
int largest  = numbers.Max();  // 32
int smallestMod = numbers.Max(n => n % 10);  // 8
Purchase runtimeError = dbContext.Purchases.Min();             // خطا
decimal? lowestPrice = dbContext.Purchases.Min(p => p.Price);  // صحیح
Purchase cheapest = dbContext.Purchases
    .Where(p => p.Price == dbContext.Purchases.Min(p2 => p2.Price))
    .FirstOrDefault();

🔹 Sum و Average

Conventions-UsedThis-Book

decimal[] numbers  = { 3, 4, 8 };
decimal sumTotal   = numbers.Sum();     // 15
decimal average    = numbers.Average(); // 5  (میانگین)
int combinedLength = names.Sum(s => s.Length);   // 19

Conventions-UsedThis-Book

🔹 Aggregate و مسائل مرتبط

int avg = new int[] { 3, 4 }.Average(); // خطا: cannot convert double to int
double avg = new int[] { 3, 4 }.Average(); // 3.5
double avg = numbers.Average(n => (double)n);
from c in dbContext.Customers
where c.Purchases.Average(p => p.Price) > 500
select c.Name;

🔹 Aggregate

int[] numbers = { 1, 2, 3 };
int sum = numbers.Aggregate(0, (total, n) => total + n); // 6

🔹 تجمیع بدون Seed

int[] numbers = { 1, 2, 3 };
int sum = numbers.Aggregate((total, n) => total + n); // 6
int[] numbers = { 1, 2, 3 };
int x = numbers.Aggregate(0, (prod, n) => prod * n); // 0*1*2*3 = 0
int y = numbers.Aggregate((prod, n) => prod * n);   // 1*2*3 = 6

⚠️ مشکلات تجمیع بدون Seed

int[] numbers = { 2, 3, 4 };
int sum = numbers.Aggregate((total, n) => total + n * n); // 27
int[] numbers = { 0, 2, 3, 4 };
  1. بازنویسی تابع تجمیع به صورت جابجایی و ترکیبی:
int sum = numbers.Select(n => n * n).Aggregate((total, n) => total + n);
Math.Sqrt(numbers.Average(n => n * n));
double mean = numbers.Average();
double sdev = Math.Sqrt(numbers.Average(n => {
    double dif = n - mean;
    return dif * dif;
}));

🔹 Quantifiers

IEnumerable<TSource>bool

Conventions-UsedThis-Book

🔹 Contains و Any

مثال‌ها:

bool hasAThree = new int[] { 2, 3, 4 }.Contains(3);       // true
bool hasAThree = new int[] { 2, 3, 4 }.Any(n => n == 3);  // true
bool hasABigNumber = new int[] { 2, 3, 4 }.Any(n => n > 10); // false
bool hasABigNumber = new int[] { 2, 3, 4 }.Where(n => n > 10).Any();
from c in dbContext.Customers
where c.Purchases.Any(p => p.Price > 1000)
select c

🔹 All و SequenceEqual

dbContext.Customers.Where(c => c.Purchases.All(p => p.Price < 100));

🔹 Generation Methods

voidIEnumerable<TResult>

Conventions-UsedThis-Book

🔹 Empty, Repeat و Range

متدهای Empty، Repeat و Range متدهای ایستا (static) هستند و توالی‌های ساده محلی را تولید می‌کنند.


🔹 Empty

متد Empty یک توالی خالی تولید می‌کند و تنها نیاز به نوع داده دارد:

foreach (string s in Enumerable.Empty<string>())
    Console.Write(s);   // <چیزی نمایش داده نمی‌شود>

در ترکیب با عملگر ??، Empty عکس DefaultIfEmpty عمل می‌کند.

مثال: فرض کنید یک آرایه‌ی jagged از اعداد صحیح داریم و می‌خواهیم همه‌ی اعداد را در یک لیست صاف جمع کنیم. کوئری SelectMany زیر در صورت وجود آرایه‌ی null داخلی با خطا مواجه می‌شود:

int[][] numbers =
{
    new int[] { 1, 2, 3 },
    new int[] { 4, 5, 6 },
    null                     // این null باعث شکست کوئری می‌شود
};

IEnumerable<int> flat = numbers.SelectMany(innerArray => innerArray);

استفاده از Empty همراه با ?? مشکل را حل می‌کند:

IEnumerable<int> flat = numbers
    .SelectMany(innerArray => innerArray ?? Enumerable.Empty<int>());

foreach (int i in flat)
    Console.Write(i + " ");     // 1 2 3 4 5 6

🔹 Range و Repeat

foreach (int i in Enumerable.Range(5, 3))
    Console.Write(i + " ");    // 5 6 7
foreach (bool x in Enumerable.Repeat(true, 3))
    Console.Write(x + " ");    // True True True