المفاهيم
هنفهم إيه؟ لما method تستقبل مجموعة، نوعها يحدد هل تقدر تقرا أو تضيف أو تبحث بكفاءة. هنقارن collections الشائعة، وبعدها نستخدم للتحويل والتصفية والتجميع، ونفهم إمتى الاستعلام يتنفذ فعلاً وإزاي ده يغيّر الأداء.
قبل ما تبدأ: collection حاوية لعناصر، والـ array أو List أمثلة عليها. loop يمر على العناصر واحدة واحدة؛ LINQ يكتب عمليات زي Where وSelect بصورة أوضح. لو دي أول مرة تشوفها، ابدأ بأول كارت واقرأ الأمثلة كبديل للـ loops.
المصطلحات اللي تحتها خط منقّط تقدر تضغط عليها لمعرفة معناها، وترجع تكمل من نفس مكانك.
#اختار أقل collection contract يعبّر عن المطلوبChoose the collection contract you need
لما method محتاجة تقرا عناصر بس، إجبار المستدعي يبعت List بيضيّق الاستخدام من غير فايدة. اختار interface حسب العمليات المطلوبة، من غير ما توعد بـ مش موجودة.
IEnumerable<T>
+-- ICollection<T> Count, Add, Remove, IsReadOnly
| +-- IList<T> indexed get/set, Insert, RemoveAt
| +-- ISet<T> set operations
+-- IReadOnlyCollection<T> Count
| +-- IReadOnlyList<T> indexed get
+-- IQueryable<T> Expression, Provider, ElementType
IReadOnlyList<T> does not inherit IList<T>.
static decimal Total(IEnumerable<decimal> prices) => prices.Sum();
var mutable = new List<int> { 1, 2 };
IReadOnlyList<int> view = mutable; // (1)
mutable.Add(3);
Console.WriteLine(view.Count); // (2) 3
- (1) الـ view ما بيوفّرش Add، لكن لسه بيشاور على نفس List.
- (2) التغيير من reference تاني ظاهر؛ read-only interface مش snapshot ولا deep immutability.
مش وعد إن المصدر lazy أو رخيص أو قابل للتكرار. ICollection ممكن يكون fixed-size أو read-only، وساعتها عمليات التعديل ترمي NotSupportedException. ماتعدّلش List أثناء foreach؛ استخدم RemoveAll أو iteration محسوبة.
#Array و List و LinkedList: تكلفة العملياتArray, List and LinkedList costs
اختيار collection حسب عبارة «الإضافة فيها أسرع» من غير تحديد المكان بيؤدي لقرار غلط. لازم تحسب تكلفة الوصول للمكان كمان.
| النوع | قراءة index | إضافة ومسح | المقابل |
|---|---|---|---|
| T[] | O(1) | الحجم ثابت؛ الإزاحة أو النسخ O(n) | تخزين متصل، مناسب لحجم معروف |
| List<T> | O(1) | Add في الآخر O(1) amortized؛ وسط القائمة O(n) | capacity زيادة ونسخ وقت resize |
| LinkedList<T> | O(n) للوصول لمكان | O(1) حوالين node معروف؛ البحث O(n) | لكل node و locality أضعف |
var ids = new List<int>(capacity: 1000); // (1)
ids.Add(10);
ids.Add(20);
ids.RemoveAll(id => id < 15); // (2)
var queue = new LinkedList<int>();
var node = queue.AddLast(20);
queue.AddBefore(node, 10); // (3)
- (1) Capacity مش Count؛ بتجهز مساحة لو العدد معروف تقريباً.
- (2) مسح بشرط من غير تعديل أثناء foreach.
- (3) معانا node بالفعل، فالإضافة O(1). لو دورنا عليه الأول لازم نحسب البحث.
ابدأ بـ List للاستخدام العام. Queue و Stack أوضح من LinkedList لو المطلوب أو بس.
#Dictionary و HashSet: فهرس بدل البحث المتكررDictionary and HashSet: index repeated lookups
بدل ما تلف على كل المنتجات مع كل order line، اعمل فهرس مرة. Dictionary بيربط key بقيمة، و HashSet بيجاوب «موجود؟» وبيمنع التكرار حسب comparer.
foreach (var line in lines)
{
var product = products.First(p => p.Id == line.ProductId);
Console.WriteLine(product.Name);
}var byId = products.ToDictionary(p => p.Id); // (1)
foreach (var line in lines)
{
if (byId.TryGetValue(line.ProductId, out var p)) // (2)
Console.WriteLine(p.Name);
}var emails = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
bool added = emails.Add("buyer@example.com");
bool duplicate = emails.Add("BUYER@example.com"); // false
- (1) بناء الفهرس O(m) في المتوسط، و ToDictionary بيرمي لو keys متكررة.
- (2) lookup متوسط O(1)، فالإجمالي المتوقع O(m + n) بدل O(m × n)، مقابل memory زيادة.
Hash collisions ممكن تخلّي العملية O(n) في أسوأ حالة. List.Contains بحث O(n)، و SortedDictionary عملياته O(log n) ومرتّب بالـ key. Equal keys لازم نفس hash؛ ماتعدّلش fields داخلة في hash أثناء وجود key في collection، وماتعتمدش على ترتيب Dictionary كـ contract.
#IEnumerable و IQueryable والـ SQL الناتجIEnumerable, IQueryable and generated SQL
- افترض جدولًا فيه 10,000 طلب، و20 طلبًا فقط إجماليهم أكبر من 1,000. المطلوب عرض رقم وإجمالي العشرين طلبًا.
- لما Where وSelect يكونا قابلين للترجمة ويعملان قبل AsEnumerable، يقدر يرسل الفلتر واختيار الأعمدة لقاعدة البيانات، فترجع البيانات المطلوبة فقط.
- لو انتقلت إلى Enumerable قبل Where، الفلتر ده يعمل داخل التطبيق عند قراءة النتائج؛ وقد تنقل طلبات لا تحتاجها. وصف للعمليات يقرأه المترجم إلى ؛ دالة ينفذها كود .
مع EF Core زي طلب محدد للمخزن: «هات الأعمدة دي للطلبات الكبيرة». يعرّف iteration، و Enumerable operators اللي بعد الحد الفاصل بتشتغل بكود .NET على العناصر الراجعة.
var big = db.Orders
.AsEnumerable() // (1)
.Where(o => o.Total > 1000m)
.Select(o => new { o.Id, o.Total })
.ToList();var query = db.Orders
.Where(o => o.Total > 1000m) // (2)
.Select(o => new { o.Id, o.Total });
Console.WriteLine(query.ToQueryString()); // (3)
var big = await query.ToListAsync(ct);-- Representative SQL Server output; aliases vary by provider/version.
-- Before: all mapped order columns, no Total predicate.
SELECT [o].[Id], [o].[CustomerId], [o].[Total], [o].[Status]
FROM [Orders] AS [o];
-- After:
SELECT [o].[Id], [o].[Total]
FROM [Orders] AS [o]
WHERE [o].[Total] > 1000.0;
- (1) AsEnumerable مش بيعمل execute ولا buffering لوحده؛ بيغيّر اختيار operators اللي بعده، والفلترة هنا client-side وقت iteration.
- (2) Queryable.Where بياخد expression tree يقدر provider يترجمه؛ Enumerable.Where بياخد delegate.
- (3) ToQueryString للتشخيص، والتنفيذ في ToListAsync. الأمثلة محتاجة EF Core و فيه Orders.
مش كل IQueryable مصدره SQL، ومش كل expression قابل للترجمة. EF Core الحديث بيرفض غالباً تعبير غير قابل للترجمة في Where؛ top-level projection ممكن يحتوي client evaluation. خلي query composition داخل حدود عمر DbContext، وراجع SQL وخطة التنفيذ.
#Deferred execution وفخ إعادة enumerationDeferred execution and repeated enumeration
- عندك الأرقام 1 و2 و3، ووصفت استعلامًا يرجع الأكبر من 2. كتابة الاستعلام المؤجل وحدها لا تعمل نسخة من الأرقام ولا تبدأ الفلترة.
- أضف 4 للمصدر ثم اقرأ الاستعلام: النتيجة 3 و4. المصدر اتغير قبل التنفيذ، والاستعلام رأى حالته وقت القراءة.
- لو نفذت ToList قبل إضافة 4، تكون خزنت النتيجة وقتها: 3 فقط. دي materialization؛ تثبيت عناصر النتيجة لا يعني نسخًا عميقًا لكل object داخلها.
الـ query زي وصفة؛ كتابتها مش معناها إن الأكل اتعمل. Where و Select مؤجلين وبيشتغلوا تدريجياً؛ OrderBy و GroupBy في -to-Objects مؤجلين لكن محتاجين buffering قبل أول نتيجة.
var ids = new List<int> { 1, 2, 3 };
var query = ids.Where(id => id > 1); // (1)
ids.Add(4);
Console.WriteLine(query.Count()); // (2) 3
var snapshot = query.ToArray(); // (3)
ids.Add(5);
Console.WriteLine(snapshot.Length); // 3
var paid = db.Orders.Where(o => o.Status == "Paid");
bool exists = await paid.AnyAsync(ct); // (4) query one
decimal total = await paid.SumAsync(o => o.Total, ct); // query two
- (1) لسه مفيش filtering.
- (2) Count شغّل الوصفة وشاف المصدر بعد الإضافة.
- (3) ToArray نفّذ فوراً وثبّت العناصر، لكن objects نفسها مش deep copies.
- (4) كل terminal operator هنا query مستقلة؛ المصدر ممكن يتغير بينهم.
لو محتاج العناصر أكتر من مرة وحجمها مناسب، مرة. لو محتاج total بس، شيل Any واعمل في database بدل تحميل الجدول. مش بيضمن إن مصدر file أو network قابل لإعادة القراءة. Immediate operators تشمل ToList، و ToDictionary، و First، و Any، و Sum.
#Select و SelectMany و GroupByProjection, flattening and grouping
اختار شكل النتيجة الأول: عنصر لكل order، ولا كل lines في sequence واحدة، ولا مجموع لكل customer؟ دي تلات عمليات مختلفة.
var totals = orders.Select(o => o.Total); // (1)
var lines = orders.SelectMany(o => o.Lines); // (2)
var detail = orders.SelectMany(o => o.Lines,
(o, line) => new { OrderId = o.Id, line.ProductId });
var revenue = orders.GroupBy(o => o.CustomerId) // (3)
.Select(g => new
{
CustomerId = g.Key,
Count = g.Count(),
Total = g.Sum(o => o.Total)
});
- (1) Select يحوّل كل عنصر لنتيجة واحدة، حتى لو النتيجة نفسها collection.
- (2) SelectMany يفرد الـ nested collections؛ overload التاني بيحتفظ بسياق الـ order.
- (3) GroupBy ينتج groups ليها Key، وبعدها نحسب aggregates.
في memory، GroupBy بيحتفظ بالعناصر في مجموعات. لو محتاج counts بس شوف CountBy. في ، grouping مع scalar aggregates غالباً يترجم لـ GROUP BY؛ إرجاع groups كاملة مش نفس شكل . ماتضمنش ترجمة أي SelectMany: شكل التعبير والـ provider مهمين.
#Join و LeftJoin في .NET 10Join and .NET 10 LeftJoin
Inner join بيحذف اللي ملوش match. تقرير العملاء لازم أحياناً يعرض العميل حتى لو معندوش orders؛ هنا محتاج left join، والـ right side ممكن يبقى null.
var matched = customers.Join(orders, // (1)
c => c.Id, o => o.CustomerId,
(c, o) => new { c.Name, OrderId = o.Id });
var all = customers.LeftJoin(orders, // (2) .NET 10
c => c.Id, o => o.CustomerId,
(c, o) => new { c.Name, OrderId = o == null ? (int?)null : o.Id });
// Before .NET 10:
var old = from c in customers
join o in orders on c.Id equals o.CustomerId into matches
from o in matches.DefaultIfEmpty() // (3)
select new { c.Name, OrderId = o == null ? (int?)null : o.Id };
- (1) كل pair مطابق ينتج row؛ لو العميل له 3 orders يطلع 3 مرات.
- (2) LeftJoin يحافظ على العميل من غير order، والمثال يفترض Order class و Id نوعه int.
- (3) GroupJoin مع DefaultIfEmpty هو الشكل القديم المكافئ.
| قديم | جديد |
|---|---|
| GroupJoin و SelectMany و DefaultIfEmpty | LeftJoin و RightJoin في 10؛ أوضح في التعبير عن الاتجاه |
مع استخدم 10 و provider متوافق، وراجع . Join مش بيضمن إزالة التكرار، و WHERE على أعمدة الجانب اليمين بعد left join ممكن يستبعد missing rows لو الشرط مش بيسمح بـ null.
#First و Single و Any و AllElement selection and quantifiers
اختيار operator بيعبّر عن contract: «هات أول واحد» غير «لازم واحد بالظبط». ما تخبيش duplicate bug باستعمال First لمجرد إنه أسهل.
| Operator | مفيش match | أكتر من match |
|---|---|---|
| First | exception | أول match |
| FirstOrDefault | default(T) | أول match |
| Single | exception | exception |
| SingleOrDefault | default(T) | exception |
bool hasPaid = orders.Any(o => o.Status == "Paid"); // (1)
bool allPaid = orders.All(o => o.Status == "Paid"); // (2)
bool nonEmptyAndPaid = orders.Count > 0 && allPaid; // List<Order>
int absent = Array.Empty<int>().FirstOrDefault(); // (3) 0
- (1) Any مع predicate يقف عند أول match؛ مع عادةً EXISTS.
- (2) All يقف عند أول مخالفة، ويرجع true على empty sequence.
- (3) default مش دايماً null؛ للـ int هو صفر، وممكن يبقى قيمة حقيقية.
Count property في List بتتكلف O(1)، و Count() ممكن يستفيد من collection count؛ مش كل Count بيعمل scan. Single مش بديل لـ unique constraint في database، و First محتاج ترتيب صريح لو معنى «الأول» مهم.
#الترتيب والـ pagination: offset و keysetOrdering and pagination: offset versus keyset
صفحات الطلبات محتاجة ترتيب ثابت وفريد، وإلا عناصر تتكرر أو تختفي حتى من غير bug في الواجهة. OrderBy يبدأ ترتيب أساسي جديد؛ ThenBy يضيف tie-breaker.
int pageSize = 20;
var offsetPage = await db.Orders
.OrderBy(o => o.CreatedAt).ThenBy(o => o.Id) // (1)
.Skip((pageNumber - 1) * pageSize).Take(pageSize)
.Select(o => new { o.Id, o.CreatedAt, o.Total })
.ToListAsync(ct);
var nextPage = await db.Orders
.Where(o => o.CreatedAt > lastTime || // (2)
(o.CreatedAt == lastTime && o.Id > lastId))
.OrderBy(o => o.CreatedAt).ThenBy(o => o.Id)
.Take(pageSize).ToListAsync(ct);
- (1) Id يكسر التعادل لو CreatedAt متساوي؛ المثال بيفترض Id رقمي. تحقق من pageNumber وحدّد أقصى pageSize.
- (2) Cursor فيه الوقت والـ Id مع بعض. predicate لازم يطابق اتجاه الترتيب، و index يبدأ بنفس الأعمدة.
Offset يسمح بالقفز لصفحة برقم، لكن deep offsets بتعمل شغل أكتر وصفحاتها بتتزحزح مع inserts و deletes. Keyset مناسب Next و Previous ومش بيعدّي على كل الصفوف السابقة، لكنه مش بيوفّر قفز عشوائي بسهولة. لو sort key اتغير أثناء التصفح، لسه ممكن يحصل تغيير في النتائج؛ cursor مش snapshot .
#Chunk و DistinctBy و CountBy و AggregateByBatching, deduplication and per-key aggregates
لو محتاج تبعت batches أو counts بس، مش لازم تبني groups كاملة أو تكتب loops متداخلة. الأمثلة دي -to-Objects؛ وجود operator في Enumerable مش ضمان إن يترجمه.
foreach (var batch in orders.Chunk(100)) // (1) .NET 6
Console.WriteLine(batch.Length);
var unique = products.DistinctBy(p => p.Sku,
StringComparer.OrdinalIgnoreCase); // (2) .NET 6
var counts = orders.CountBy(o => o.CustomerId); // (3) .NET 9
var totals = orders.AggregateBy(o => o.CustomerId,
seed: 0m, func: (sum, order) => sum + order.Total); // (4) .NET 9
- (1) Chunk يطلع arrays، وآخر batch ممكن أصغر. ده مش bulk insert ولا تلقائي.
- (2) DistinctBy يختار uniqueness بالـ key؛ لو اختيار ممثل بعينه مهم، رتّب أو اختار صراحةً.
- (3) CountBy يطلع KeyValuePair لكل key وعدده بدل الاحتفاظ بكل عناصر group.
- (4) AggregateBy يحتفظ accumulator لكل customer. الـ memory بتزيد مع عدد keys، ولازم المصدر يتقري قبل النتائج المجمعة.
البديل القديم CountBy هو GroupBy ثم Count؛ و AggregateBy هو GroupBy ثم . Chunk على query بعد AsEnumerable مش database pagination، ولو بتعمل لكل الجدول الأول فقدت فايدة streaming.
#Frozen و immutable collections والمشاركة بين threadsFrozen, immutable and concurrent collections
لو catalog للقراءة الكتير والتحديث النادر، كل reader مش محتاج lock حوالين dictionary بيتغير. ابنِ snapshot مناسب وحدد إزاي هتنشر النسخة الجديدة بأمان.
using System.Collections.Frozen;
using System.Collections.Immutable;
var prices = new Dictionary<int, decimal> { [10] = 120m };
FrozenDictionary<int, decimal> frozen = prices.ToFrozenDictionary(); // (1)
var original = ImmutableDictionary<int, decimal>.Empty.Add(10, 120m);
var changed = original.SetItem(10, 125m); // (2)
Console.WriteLine(original[10]); // 120
Console.WriteLine(changed[10]); // 125
- (1) Frozen من 8؛ بناء بتكلفة أعلى مقابل تحسين repeated lookups، ومن غير updates بعد البناء.
- (2) ImmutableDictionary بيرجع نسخة منطقية جديدة مع structural sharing؛ الأصل ما اتغيرش.
Frozen مناسب build-once/read-many؛ مناسب إصدارات متعددة مع updates. الاتنين مش بيخلّوا mutable values جواهم immutable. لو عايز shared mutable collection، شوف ConcurrentDictionary و Channels في موضوع 04: Async و multithreading. Immutable snapshot كمان محتاج atomic publication لو أكتر من writer بيحدّثه.
أسئلة الانترفيو
مفيش نتايج
راجع فهمك
ليه الاختيار ده مناسب؟ إمتى ممكن ماينفعش؟ وإيه اللي هيتغير لو الحمل زاد أو ظهر فشل؟
5.1What is LINQ, and are method and query syntax different execution engines?Junior
تركيب queries typed على مصادر مختلفة. Query syntax بتتحوّل لنداءات operators؛ التنفيذ بيتحدد بالمصدر والـ provider، مش شكل الكتابة.
I use to compose strongly typed filtering, , grouping and aggregation. Method syntax calls operators directly, while query syntax is translated by the compiler into operator calls. They are not separate execution engines. The source and selected operators determine behavior: Enumerable normally executes over objects, while Queryable builds expressions for a provider. I choose the syntax that makes the query readable and inspect provider behavior when the source is a database.
5.2How do IEnumerable, ICollection and IList differ?Junior
للـ iteration؛ ICollection بيضيف Count وعمليات collection؛ IList بيضيف index. Read-only interfaces خط تاني مش ضمان إن الأصل .
exposes iteration. ICollection adds a count and collection operations such as Add and Remove, although a read-only implementation can reject mutations. IList adds indexed access and position-based operations. I choose the smallest contract that expresses what the method needs. IReadOnlyCollection and IReadOnlyList expose reading without mutation members, but they do not guarantee an object: another reference may still change the backing collection, so I document snapshot requirements separately.
5.3When would you choose Array, List or LinkedList?Junior
Array لحجم ثابت، و List الافتراضي العملي. LinkedList ينفع للتعديل حوالين node معروف؛ الوصول للـ node نفسه ممكن O(n).
I use an array when the size is fixed and indexed storage is appropriate, and a List for a growable sequence with frequent reads. List append is amortized constant time, but middle insertion shifts elements. LinkedList can insert or remove in constant time when I already hold the node; finding that node is linear. Its and poor locality also matter. I would usually choose Queue or Stack directly for simple or requirements.
5.4Why are Dictionary and HashSet useful for repeated lookups?Junior
Dictionary للـ key والقيمة؛ HashSet للـ membership. المتوسط O(1)، لكن hashing و equality والـ collisions مهمين، والتكلفة فيها memory.
I use Dictionary for key-to-value lookup and HashSet for membership and uniqueness. Their usual lookup cost is constant time on average, compared with a linear scan through a List. Building one index can replace repeated nested scans and reduce expected total work substantially. That trades extra memory and construction cost for faster reuse. I also choose the equality comparer deliberately and avoid mutable keys, because collisions and inconsistent hashing can undermine performance or correctness.
5.5What is the difference between Select and SelectMany?Junior
Select نتيجة واحدة لكل input؛ SelectMany بيفرد sequence داخل كل input. Order من غير lines يساهم بصفر عناصر في SelectMany.
Select maps each input to one result. If I select an order's Lines property, the result is a sequence of line collections. SelectMany maps each input to a sequence and flattens those sequences, so I get all order lines together. An order with no lines contributes no elements. When I also need the order identifier, I use the result-selector overload to combine the outer order with each inner line.
5.6How do First and Single express different requirements?Junior
First واحد على الأقل؛ Single واحد بالظبط. OrDefault يسمح بصفر، مش بيغيّر موقف Single من التكرار، و default للـ int صفر.
First asks for the first match and throws when none exists. Single requires exactly one match and also throws when duplicates exist. Their OrDefault forms permit an empty result, but SingleOrDefault still rejects multiple matches. I use Single when duplicate data violates the operation's contract, while also enforcing uniqueness in the database. I remember that default is type-dependent, and I specify ordering if the identity of the first row matters.
5.7Why does All return true on an empty sequence?Junior
مفيش عنصر خالف الشرط، فـ All true. لو المطلوب موجودين وكلهم paid، لازم non-empty check منفصل، مع مراعاة تكلفة إعادة enumeration.
All returns true because there is no element that violates the predicate. I would not use it alone to mean that an order has at least one line and every line is valid. That rule needs both non-emptiness and the universal condition. On a materialized List I can inspect Count and then call All. On an expensive deferred source, I consider a single-pass implementation or an appropriate database expression to avoid repeated enumeration.
5.8What changes when IQueryable becomes IEnumerable before filtering?Mid
Operators اللي بعد الحد الفاصل تبقى Enumerable . AsEnumerable لوحده لا بينفذ ولا يحمل؛ وقت enumeration الفلتر ده بيشتغل على الصفوف الراجعة.
The later Where binds to Enumerable and receives a rather than an for translation. With an source, the database query before that boundary still runs on enumeration, but the later filter runs in over returned rows. AsEnumerable itself does not execute or buffer. I keep translatable filters and projections before the boundary, inspect generated , and move to client processing only after deliberately limiting the server result.
5.9What is deferred execution, and is every deferred operator streaming?Mid
Deferred يعني التنفيذ يتأجل للـ enumeration؛ streaming يعني أول نتيجة ممكن تطلع من غير قراءة المصدر كله. OrderBy مؤجل لكن محتاج buffering.
means constructing a query does not immediately process its source. Enumeration or a terminal operator triggers the work. It does not necessarily mean streaming: Where and Select can yield progressively, while -to-Objects OrderBy must buffer before producing sorted results. I consider both timing and memory behavior. Re-enumerating can repeat expensive work or see changed data, so I a reasonably sized result when I need a stable, reusable snapshot.
5.10How would you fix Any followed by Sum on the same EF query?Mid
لو محتاج total بس، شيل Any. لو محتاج العناصر للتكرار وحجم مناسب، مرة. مش كل query مرتين غلط؛ counts مع pages ممكن مقصود.
I first ask what result is actually needed. If the caller only needs the total, I issue the directly and remove the existence query. If it needs the rows several times and their size is bounded, I once and reuse them. I would not load an entire table just to save one round trip. Separate count and page queries can be intentional, but I make their cost and possible consistency differences explicit.
5.11When is Any preferable to Count greater than zero?Mid
Any مع predicate بيقف عند أول match، وغالباً EXISTS. لكن List.Count property O(1)، و Count() ممكن يستفيد من count متاح.
When I need existence, Any expresses that intent and can stop at the first matching element; commonly translates it to EXISTS. Counting matching rows asks for more information than needed. I avoid saying that every Count scans everything, though: List.Count is a constant-time property and some Enumerable.Count calls use a known collection count. The choice should follow the required result, the source type and, for databases, the generated and execution plan.
5.12What happens if you call OrderBy twice instead of ThenBy?Mid
OrderBy التاني يحدد primary ordering جديد. ThenBy بيكمّل الترتيب جوه المتساويين. Stability في memory مش بديل عن tie-breaker واضح.
A second OrderBy establishes a new primary ordering rather than adding a secondary key. I use ThenBy or ThenByDescending for tie-breaking, such as ordering orders by creation time and then identifier. -to-Objects sorting is stable, so previous order can still affect ties, but I do not rely on that to express a composite sort. For database paging I explicitly include a unique tie-breaker because row order is otherwise not deterministic.
5.13How does LeftJoin differ from Join in .NET 10?Mid
Join يطلع matching pairs بس؛ LeftJoin يحافظ على left بلا match. القديم GroupJoin مع DefaultIfEmpty. RightJoin الاتجاه العكسي.
Join produces matching pairs and excludes unmatched outer elements. LeftJoin, introduced in 10, retains every left element and supplies a default right value when nothing matches. For reference-type orders that means handling null in the . The older form uses GroupJoin followed by SelectMany and DefaultIfEmpty. I also remember that multiple matches produce multiple rows and that a later right-side filter can accidentally exclude the unmatched rows I intended to preserve.
5.14What do Chunk, DistinctBy, CountBy and AggregateBy solve?Mid
Chunk batches، و DistinctBy uniqueness حسب key؛ CountBy و AggregateBy تجميع لكل key من غير الاحتفاظ بكل group. translation لازم تتراجع.
I use Chunk to consume a sequence in arrays of bounded batch size and DistinctBy to deduplicate by a selected key. CountBy and AggregateBy, added in 9, maintain per-key counts or accumulators without retaining all group elements as GroupBy does. They still need memory proportional to distinct keys. I treat these as source-specific tools: using an Enumerable operator does not guarantee database translation, and Chunk alone does not create bulk or durable processing.
5.15How would you design stable pagination for a large changing order list?Senior
OrderBy فريد، و cursor فيه كل sort keys، و index مطابق. Keyset يقلل مشاكل shift، لكن تغيير sort keys مش بيختفي بالسحر.
I use a deterministic ordering, for example CreatedAt followed by a unique numeric Id, with a matching index. A keyset cursor carries both values and the next-page predicate compares them lexicographically in the same sort direction. This avoids deep offset scans and many shifts caused by new rows. The trade-off is harder random page access. I also define behavior when sort keys change, because a cursor does not provide snapshot across separate requests.
5.16How do frozen and immutable collections differ?Senior
Frozen بيصرف وقت في البناء لتحسين القراءة؛ يسمح بإصدار جديد مع structural sharing. القيم mutable ممكن تتغير في الاتنين.
I choose frozen collections for data built infrequently and read repeatedly, because they optimize lookup after construction and offer no update path. collections support producing new logical versions while preserving old ones, often with structural sharing. Both protect collection structure, not necessarily mutable objects stored inside it. When replacing a shared snapshot, I still coordinate writers and publish the new reference safely; immutability does not automatically make a multi-step read-modify-write operation atomic.
5.17What can make an apparently correct LINQ query fail against EF Core?Senior
-to-Objects يقدر ينفذ method عادية، لكن provider محتاج ترجمة. Collation و null semantics وأنواع joins ممكن تختلف؛ وخطة التنفيذ هم الدليل.
An expression that executes perfectly over objects may have no translation in the database provider. A custom method in Where is a common example. Provider support, null semantics and database collation can also affect behavior. I inspect ToQueryString, test against the real provider and review the execution plan rather than relying only on in-memory tests. If client processing is necessary, I first apply server-side restrictions and so the boundary does not pull excessive data.
5.18Does a read-only collection interface make sharing across threads safe?Senior
لأ؛ ممكن فيه reference بيعدّل الأصل، أو elements mutable. اختار snapshot و safe publication، أو synchronization مناسب؛ موضوع 04 فيه concurrent collections.
No. IReadOnlyList only restricts operations through that reference; another owner can still mutate the underlying List, and contained objects can remain mutable. For stable shared data I prefer an or frozen snapshot with deliberate publication. For changing shared state I use synchronization or a concurrent collection whose guarantees match the operation. I also distinguish atomic collection methods from a larger business workflow, because several individually safe calls are not automatically one atomic .
اختبر نفسك
إجابات الاختبار وعلامات إكمال المحاور بتتحفظ على المتصفح ده، ومش بتتنقل لأجهزة تانية.
What does AsEnumerable do by itself?
What does an empty sequence's All(predicate) return?
What does SingleOrDefault do when two items match?
Which operation is O(1) for LinkedList when the node is already known?
Average Dictionary lookup and worst-case collision-heavy lookup are respectively...
Which expression adds a secondary sort key?
A cursor sorts by CreatedAt and Id. What must it carry?
Which .NET 9 operator counts elements per key without retaining full groups?
What does a left join return for a customer with no matching Order class instance?
Which collection suits a catalog built once and queried many times?