الوظيفة:
مطور دوت نت
المستوى:
خبرة متوسطة
عدد الأسئلة:
١٢ سؤالاً
الزمن:
٦ دقائق

أسئلة انترفيو ASP.NET Core وC# لأصحاب الخبرة

مواقف عملية تُطرح في مقابلات وظيفة مطور دوت نت لأصحاب الخبرة المتوسطة: أساسيات C#، والبرمجة الكائنية، وASP.NET Core، وEntity Framework وLINQ، وSQL Server، والبرمجة غير المتزامنة. في آخرها تقرير بنقاط ضعفك.

المهارات: أساسيات لغة C#، إطار Entity Framework وLINQ، البرمجة غير المتزامنة والأداء، تطبيقات ASP.NET Core وWeb API، البرمجة كائنية التوجه وأنماط التصميم، قواعد بيانات SQL Server

تعليمات: لكل سؤال إجابة صحيحة واحدة. إن لم تعرف الإجابة فاختر «لا أعرف»، وسيدلّك التقرير على ما تراجعه.

السؤال الأول: بافتراض التعريف public record Person(string Name, int Age);، ماذا يطبع الكود التالي؟ var p1 = new Person("Sara", 30); var p2 = new Person("Sara", 30); Console.WriteLine(p1 == p2); Console.WriteLine(ReferenceEquals(p1, p2));

اختر إجابتك لتبدأ الاختبار

كل الأسئلة مع الإجابات (١٢ سؤالاً)

جرّب المقابلة أولاً، ثم افتح الإجابات لتراجعها مع الشرح.
  1. ١. بافتراض التعريف public record Person(string Name, int Age);، ماذا يطبع الكود التالي؟ var p1 = new Person("Sara", 30); var p2 = new Person("Sara", 30); Console.WriteLine(p1 == p2); Console.WriteLine(ReferenceEquals(p1, p2));

    • True ثم True
    • True ثم False
    • False ثم False
    • خطأ ترجمة، لأن المعامل == غير معرّف للنوع record

    الشرح: يولّد المترجم للنوع record مساواة قائمة على القيم، فيقارن == قيم الخصائص ويعطي True، بينما يبقى الكائنان نسختين منفصلتين في الذاكرة فيعطي ReferenceEquals القيمة False. يخطئ كثيرون لأنهم يتوقعون من record مقارنة المراجع كما في الصنف class العادي.

  2. ٢. فعّلت في ملف المشروع الإعداد <Nullable>enable</Nullable>، ثم كتبت دالة نوع إرجاعها string وتُرجع null في أحد المسارات. ماذا يحدث مع بقاء بقية إعدادات المشروع على قيمها الافتراضية؟

    • يحوّل وقت التشغيل القيمة null تلقائياً إلى سلسلة فارغة
    • يفشل بناء المشروع بخطأ ترجمة يمنع إنشاء الملف التنفيذي
    • يُرمى الاستثناء ArgumentNullException وقت التشغيل عند إرجاع null
    • يُصدر المترجم تحذيراً، ولا يتغير سلوك البرنامج وقت التشغيل

    الشرح: ميزة nullable reference types تحليل ساكن يجري وقت الترجمة فقط، فيُصدر المترجم تحذيراً مثل CS8603 دون أي فحص إضافي وقت التشغيل. يظن بعضهم أنها تمنع البناء أو تحمي البرنامج وقت التشغيل، وهذا لا يحدث إلا إذا حُوّلت التحذيرات إلى أخطاء بالإعداد TreatWarningsAsErrors.

  3. ٣. نقطة نهاية endpoint في Web API تعرض قائمة منتجات للقراءة فقط وتستقبل طلبات كثيرة، فاقترح زميلك إضافة AsNoTracking() إلى استعلام EF Core. ما الذي تفعله هذه الدالة فعلاً؟

    • لا يسجّل الكيانات المُرجعة في متتبع التغييرات، فيقل استهلاك الموارد
    • يخزّن النتائج مؤقتاً فلا يُرسل الاستعلام إلى قاعدة البيانات في الطلبات التالية
    • يؤجل تحميل الخصائص الملاحية navigation properties حتى أول وصول إليها
    • يقرأ البيانات دون قفل الصفوف، تماماً كالتلميح NOLOCK في SQL Server

    الشرح: AsNoTracking() يجعل EF Core لا يتتبع الكيانات الناتجة، فتصبح القراءة أسرع وأخف لكن تعديلاتها لا تُحفظ عبر SaveChanges(). يخلط كثيرون بينه وبين NOLOCK أو التخزين المؤقت، مع أنه لا يغير شيئاً في أقفال قاعدة البيانات ولا يمنع إرسال الاستعلام في كل مرة.

  4. ٤. في تطبيق يستخدم EF Core مع تفعيل التحميل الكسول lazy loading يوجد الكود التالي: var orders = await _context.Orders.ToListAsync(); foreach (var order in orders) { Console.WriteLine(order.Customer.Name); } تُظهر السجلات استعلاماً إضافياً إلى قاعدة البيانات لجلب العميل مع كل طلب تقريباً. ما التشخيص والحل الأنسب؟

    • تتبع التغييرات يولّد الاستعلامات، والحل إضافة AsNoTracking() إلى الاستعلام
    • الاستدعاء غير المتزامن هو السبب، والحل استبدال ToListAsync() بـ ToList()
    • الحلقة التسلسلية هي السبب، والحل استبدالها بـ Parallel.ForEach
    • مشكلة N+1، والحل تحميل العملاء مسبقاً بـ Include(o => o.Customer)

    الشرح: الوصول إلى order.Customer مع التحميل الكسول يرسل استعلاماً مستقلاً لكل طلب، فيصبح المجموع استعلاماً واحداً للطلبات وN استعلاماً للعملاء، بينما يجلب Include البيانات المرتبطة في الاستعلام نفسه. تمر هذه المشكلة كثيراً دون انتباه لأن الكود يعمل صحيحاً ويبدو سريعاً على بيانات الاختبار الصغيرة.

  5. ٥. داخل خدمة تستخدم نسخة واحدة من DbContext كتب أحد المطورين الكود التالي لتسريع التحميل: var ordersTask = _context.Orders.ToListAsync(); var customersTask = _context.Customers.ToListAsync(); await Task.WhenAll(ordersTask, customersTask); عند التشغيل يظهر استثناء InvalidOperationException. ما السبب؟

    • يجب استدعاء ConfigureAwait(false) على كل مهمة قبل تمريرها إلى Task.WhenAll
    • Task.WhenAll لا يقبل مهام تُرجع نتائج من نوع List<T>
    • نسخة DbContext الواحدة لا تدعم تنفيذ عمليتين في الوقت نفسه
    • الاستعلامات غير المتزامنة في EF Core تتطلب معاملة transaction صريحة

    الشرح: لا يدعم EF Core تشغيل عمليات متوازية على نسخة DbContext نفسها، فيجب انتظار كل استعلام قبل بدء التالي، أو استخدام نسخة مستقلة لكل عملية عبر IDbContextFactory. يقع المرشحون في هذا الخطأ لأن Task.WhenAll أسلوب صحيح مع العمليات المستقلة فعلاً مثل طلبات HTTP.

  6. ٦. إجراء في ASP.NET Core يستدعي ثلاث خدمات خارجية مستقلة عبر HTTP، ويستغرق كل استدعاء نحو ثانية: var user = await _users.GetAsync(id); var orders = await _orders.GetAsync(id); var stats = await _stats.GetAsync(id); لا يعتمد أي استدعاء على نتيجة غيره. كيف تخفض زمن الاستجابة إلى نحو ثانية واحدة؟

    • تغليف كل استدعاء بـ Task.Run مع إبقاء await لكل منها بالترتيب نفسه
    • استبدال await بـ .Result لتجنب كلفة الانتقال بين الخيوط
    • بدء الاستدعاءات الثلاثة أولاً ثم انتظارها معاً بـ Task.WhenAll
    • إضافة ConfigureAwait(false) إلى كل استدعاء من الاستدعاءات الثلاثة

    الشرح: كل await متتالٍ ينتظر انتهاء الاستدعاء قبل بدء التالي فيصبح الزمن مجموع الأزمنة، أما بدء المهام أولاً ثم Task.WhenAll فيجعلها تجري في الوقت نفسه فيقترب الزمن من أطولها. لا يغير ConfigureAwait(false) ولا Task.Run أن الانتظار ما زال متتالياً، و.Result يحجز الخيط دون فائدة.

  7. ٧. خدمة في تطبيق ASP.NET Core تنشئ HttpClient جديداً داخل كتلة using عند كل طلب لاستدعاء واجهة خارجية. تحت الضغط بدأت تظهر استثناءات SocketException بسبب نفاد المقابس sockets المتاحة. ما الحل الذي توصي به مايكروسوفت؟

    • إعادة محاولة الطلب الفاشل داخل حلقة while حتى ينجح
    • استدعاء Dispose() صراحة بعد كل طلب بدل كتلة using
    • رفع الحد الأدنى لخيوط ThreadPool باستدعاء SetMinThreads
    • استخدام IHttpClientFactory عبر AddHttpClient

    الشرح: التخلص من HttpClient لا يحرر المقبس فوراً، إذ يبقى الاتصال مدة في حالة TIME_WAIT فتنفد المقابس عند إنشاء نسخة لكل طلب، بينما يعيد IHttpClientFactory استخدام معالجات الاتصال HttpMessageHandler ويدير أعمارها. يقع كثيرون في هذا لأن HttpClient يطبق IDisposable فيبدو وضعه في using هو الاستخدام الصحيح.

  8. ٨. تطبيق ASP.NET Core ينشئ إعداداته بـ WebApplication.CreateBuilder(args) دون تعديل مصادر الإعدادات الافتراضية. عُرّف المفتاح ConnectionStrings:Default في appsettings.json وفي appsettings.Production.json وفي متغير البيئة ConnectionStrings__Default، ولم يُمرَّر عبر سطر الأوامر. عند التشغيل في بيئة Production، أي قيمة يقرؤها التطبيق؟

    • يرمي التطبيق استثناءً عند بدء التشغيل بسبب تكرار المفتاح
    • القيمة الموجودة في appsettings.Production.json
    • قيمة متغير البيئة ConnectionStrings__Default
    • القيمة الموجودة في appsettings.json

    الشرح: في بيئة Production يحمّل WebApplication.CreateBuilder المصادر بالترتيب: appsettings.json ثم ملف البيئة ثم متغيرات البيئة ثم وسيطات سطر الأوامر، والمصدر الأحدث يتغلب على ما قبله. يظن كثيرون أن ملف البيئة هو الأعلى أولوية، مع أن متغيرات البيئة تتقدم عليه، ولهذا تُستخدم عادة لتمرير الأسرار وسلاسل الاتصال في الإنتاج.

  9. ٩. يحتوي الصنف OrderService على الحقل private readonly SqlOrderRepository _repo = new SqlOrderRepository();، ولذلك يصعب كتابة اختبارات وحدة unit tests له دون قاعدة بيانات حقيقية. ما إعادة التصميم الأنسب؟

    • تعريف واجهة IOrderRepository وحقنها عبر باني OrderService
    • جعل OrderService يرث من SqlOrderRepository ليستخدم دواله
    • تحويل SqlOrderRepository إلى صنف static واستدعاء دواله مباشرة
    • تطبيق نمط Singleton على SqlOrderRepository عبر خاصية Instance

    الشرح: هذا تطبيق لمبدأ عكس التبعية Dependency Inversion: يعتمد OrderService على تجريد بدل تنفيذ محدد، فيُسجَّل التنفيذ الحقيقي في حاوي حقن التبعيات ويُمرَّر تنفيذ وهمي mock في الاختبار. الخيارات الأخرى تبقي الاقتران المباشر بقاعدة البيانات، بل يجعل static وSingleton الكلاسيكي الاستبدال في الاختبار أصعب.

  10. ١٠. دالة لحساب كلفة الشحن تحتوي على switch يتفرع حسب نوع الشحن (عادي، سريع، دولي)، وكل نوع جديد يضطر الفريق إلى تعديل هذه الدالة وإعادة اختبارها. أي نمط تصميم يتيح إضافة أنواع جديدة بأصناف جديدة دون تعديل الكود القائم؟

    • نمط Observer: إشعار المكونات المشتركة كلما تغير نوع الشحن المختار
    • نمط Adapter: تحويل واجهة مكتبة شحن خارجية إلى واجهة متوافقة
    • نمط Singleton: نسخة واحدة مشتركة من حاسبة الشحن في التطبيق كله
    • نمط Strategy: تنفيذ مستقل لكل نوع شحن خلف واجهة مشتركة

    الشرح: نمط Strategy يغلّف كل خوارزمية في صنف يطبق واجهة مشتركة، فيُضاف نوع الشحن الجديد بصنف جديد دون لمس الكود الموجود، وهذا تطبيق مباشر لمبدأ مفتوح/مغلق Open/Closed. يختار بعض المرشحين أنماطاً أخرى لمجرد شهرتها، مع أنها تحل مشكلات مختلفة كالنسخة الوحيدة أو الإشعار أو توافق الواجهات.

  11. ١١. الجدول Orders فيه العمود OrderDate من نوع datetime2 وعليه فهرس nonclustered، لكن خطة تنفيذ الاستعلام التالي تُظهر Index Scan بدل Index Seek: SELECT Id, OrderDate FROM Orders WHERE YEAR(OrderDate) = 2025; ما أفضل حل يحافظ على النتائج نفسها ويتيح استخدام Index Seek؟

    • WHERE OrderDate >= '2025-01-01' AND OrderDate < '2026-01-01'
    • WHERE OrderDate BETWEEN '2025-01-01' AND '2025-12-31'
    • WHERE DATEPART(year, OrderDate) = 2025
    • إبقاء الشرط كما هو مع التلميح WITH (INDEX(IX_Orders_OrderDate))

    الشرح: تطبيق دالة مثل YEAR أو DATEPART على العمود يمنع SQL Server من البحث في الفهرس، بينما يبقي النطاق نصف المفتوح العمود كما هو فيتيح Index Seek ويشمل كل أوقات السنة. أما BETWEEN مع '2025-12-31' فيستبعد طلبات اليوم الأخير بعد منتصف الليل لأن العمود من نوع datetime2 يخزن الوقت أيضاً.

  12. ١٢. يُنفَّذ الاستعلام التالي كثيراً، وعلى الجدول Orders فهرس nonclustered على العمود CustomerId فقط: SELECT OrderDate, Total FROM Orders WHERE CustomerId = @CustomerId; تُظهر خطة التنفيذ Index Seek يتبعه Key Lookup يستهلك معظم الكلفة. ما الحل الأنسب؟

    • إضافة التلميح WITH (NOLOCK) إلى الجدول لتقليل كلفة القراءة
    • إنشاء فهرس nonclustered مستقل على كل من OrderDate وTotal
    • إعادة بناء الفهرس على CustomerId بالأمر ALTER INDEX ... REBUILD
    • إضافة OrderDate وTotal إلى الفهرس كأعمدة INCLUDE

    الشرح: Key Lookup يعني أن الفهرس يحدد الصفوف لكنه لا يحتوي الأعمدة المطلوبة، فيعود SQL Server إلى الفهرس المجمّع clustered index لكل صف. إضافة الأعمدة عبر INCLUDE تجعل الفهرس مغطياً covering index فتختفي هذه القفزات، أما إعادة البناء أو NOLOCK أو الفهارس المنفصلة فلا تعالج السبب.

المزيد