ASP.NET Core Dependency Injection أفضل الممارسات والنصائح والخدع

في هذه المقالة ، سوف أشارك خبراتي واقتراحاتي حول استخدام Dependency Injection في تطبيقات ASP.NET Core. الدافع وراء هذه المبادئ هي ؛

  • تصميم الخدمات بفعالية وتوابعها.
  • منع قضايا متعددة الخيوط.
  • منع تسرب الذاكرة.
  • منع الأخطاء المحتملة.

تفترض هذه المقالة أنك معتاد بالفعل على Dependency Injection و ASP.NET Core في مستوى أساسي. إذا لم يكن كذلك ، يرجى قراءة وثائق ASP.NET Core Dependency Injection أولاً.

مبادئ

حقن المنشئ

يستخدم حقن المنشئ للإعلان والحصول على تبعيات الخدمة على إنشاء الخدمة. مثال:

فئة الخدمات العامة
{
    IProductRepository الخاص للقراءة فقط
    المنتجات العامةالخدمة (IProductRository productRository)
    {
        _productRepository = productRepository؛
    }
    حذف الفراغ العام (int id)
    {
        _productRepository.Delete (معرف)؛
    }
}

يقوم ProductService بحقن IProductRepository باعتباره تبعية في مُنشئه ثم استخدامه داخل طريقة الحذف.

الممارسات الجيدة:

  • حدد التبعيات المطلوبة بشكل صريح في مُنشئ الخدمة. وبالتالي ، لا يمكن إنشاء الخدمة بدون تبعياتها.
  • قم بتعيين التبعية المحقونة في حقل / خاصية للقراءة فقط (لمنع تخصيص قيمة أخرى لها بطريق الخطأ داخل إحدى الطرق).

حقن الملكية

حاوية حقن التبعية القياسية في ASP.NET Core لا تدعم حقن العقار. ولكن يمكنك استخدام حاوية أخرى تدعم حقن العقار. مثال:

باستخدام Microsoft.Extensions.Logging ؛
باستخدام Microsoft.Extensions.Logging.Abstractions ؛
مساحة الاسم
{
    فئة الخدمات العامة
    {
        ILogger العامة  Logger {get؛ جلس؛ }
        IProductRepository الخاص للقراءة فقط
        المنتجات العامةالخدمة (IProductRository productRository)
        {
            _productRepository = productRepository؛
            Logger = NullLogger  .Instance؛
        }
        حذف الفراغ العام (int id)
        {
            _productRepository.Delete (معرف)؛
            Logger.LogInformation (
                $ "تم حذف منتج بمعرف = {id}") ؛
        }
    }
}

تقوم ProductService بالإعلان عن خاصية Logger مع الضابط العام. يمكن لحاوية حقن التبعية تعيين Logger إذا كانت متوفرة (مسجلة في حاوية DI من قبل).

الممارسات الجيدة:

  • استخدم خاصية الحقن فقط للتبعيات الاختيارية. هذا يعني أن الخدمة يمكن أن تعمل بشكل صحيح دون توفير هذه التبعيات.
  • استخدم Null Object Pattern (كما في هذا المثال) إن أمكن. خلاف ذلك ، تحقق دائمًا من وجود قيمة خالية أثناء استخدام التبعية.

محدد موقع الخدمة

نمط محدد موقع الخدمة هو طريقة أخرى للحصول على التبعيات. مثال:

فئة الخدمات العامة
{
    IProductRepository الخاص للقراءة فقط
    خاص للقراءة فقط ILogger  _logger؛
    خدمة المنتجات العامة (IServiceProvider serviceProvider)
    {
        _productRepository = serviceProvider
          .GetRequiredService  ()؛
        _logger = serviceProvider
          .GetService > () ؟؟
            NullLogger  .Instance.
    }
    حذف الفراغ العام (int id)
    {
        _productRepository.Delete (معرف)؛
        _logger.LogInformation ($ "تم حذف منتج بمعرف = {id}") ؛
    }
}

تقوم ProductService بحقن IServiceProvider وحل التبعيات التي تستخدمه. يطرح GetRequiredService استثناء إذا لم يتم تسجيل التبعية المطلوبة من قبل. من ناحية أخرى ، إرجاع GetService فقط فارغة في هذه الحالة.

عند حل الخدمات داخل المنشئ ، يتم إصدارها عند إصدار الخدمة. لذلك ، لا يهمك إطلاق خدمات التخلص / التخلص التي يتم حلها داخل المنشئ (تمامًا مثل المنشئ وحقن الممتلكات).

الممارسات الجيدة:

  • لا تستخدم نمط محدد موقع الخدمة كلما كان ذلك ممكنًا (إذا كان نوع الخدمة معروفًا في وقت التطوير). لأنه يجعل التبعيات ضمنية. هذا يعني أنه لا يمكن رؤية التبعيات بسهولة أثناء إنشاء مثيل للخدمة. هذا مهم بشكل خاص لاختبارات الوحدات حيث قد ترغب في الاستهزاء ببعض تبعيات الخدمة.
  • حل التبعيات في مُنشئ الخدمة إذا كان ذلك ممكنًا. يؤدي حل إحدى طرق الخدمة إلى جعل طلبك أكثر تعقيدًا وعرضة للخطأ. سوف أتناول المشاكل والحلول في الأقسام التالية.

خدمة الحياة تايمز

هناك ثلاثة فترات خدمة في ASP.NET Core Dependency Injection:

  1. يتم إنشاء خدمات عابرة في كل مرة يتم حقنها أو طلبها.
  2. يتم إنشاء خدمات النطاق لكل نطاق. في تطبيق ويب ، ينشئ كل طلب ويب نطاق خدمة منفصل جديد. وهذا يعني أن الخدمات التي يتم تحديد نطاقها يتم إنشاؤها بشكل عام لكل طلب ويب.
  3. يتم إنشاء خدمات Singleton لكل حاوية DI. هذا يعني عمومًا أنه يتم إنشاؤها مرة واحدة فقط لكل تطبيق ، ثم يتم استخدامها طوال مدة التطبيق.

حاوية DI بتتبع جميع الخدمات التي تم حلها. يتم إطلاق الخدمات والتخلص منها عند انتهاء عمرها:

  • إذا كانت الخدمة تحتوي على تبعيات ، يتم إطلاقها تلقائيًا والتخلص منها.
  • إذا كانت الخدمة تنفذ واجهة IDisposable ، فسيتم استدعاء طريقة التخلص تلقائيًا عند إصدار الخدمة.

الممارسات الجيدة:

  • تسجيل خدماتك عابرة كلما أمكن ذلك. لأنه من السهل تصميم خدمات عابرة. لا تهتم عمومًا بتعدد الخيوط وتسريبات الذاكرة وتعلم أن مدة الخدمة قصيرة.
  • استخدم عمر الخدمة التي تم تحديد نطاقها بعناية لأنه قد يكون صعبًا إذا أنشأت نطاقات خدمة تابعة أو استخدمت هذه الخدمات من تطبيق غير موجود على الويب.
  • استخدم عمر المفردة بعناية منذ ذلك الحين ، فأنت بحاجة إلى التعامل مع مشاكل الترابط المتعددة ومشاكل تسرب الذاكرة المحتملة.
  • لا تعتمد على خدمة عابرة أو نطاقية من خدمة فردية. نظرًا لأن الخدمة العابرة تصبح مثيلًا منفردًا عندما تقوم خدمة مفردة بإدخالها وقد تتسبب في حدوث مشكلات إذا لم تكن الخدمة المؤقتة مصممة لدعم مثل هذا السيناريو. تحتوي حاوية DI الافتراضية الخاصة بـ ASP.NET Core بالفعل على استثناءات في مثل هذه الحالات.

حل الخدمات في هيئة الأسلوب

في بعض الحالات ، قد تحتاج إلى حل خدمة أخرى بطريقة خدمتك. في مثل هذه الحالات ، تأكد من إطلاق الخدمة بعد الاستخدام. أفضل طريقة لضمان ذلك هي إنشاء نطاق خدمة. مثال:

الطبقة العامة PriceCalculator
{
    خاص للقراءة IServiceProvider _serviceProvider؛
    PriceCalculator العامة (IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider؛
    }
    حساب تعويم عام (منتج المنتج ، عدد العمليات ،
      اكتب taxStrategyServiceType)
    {
        باستخدام (var نطاق = _serviceProvider.CreateScope ())
        {
            var taxStrategy = (ITaxStrategy) scope.ServiceProvider
              .GetRequiredService (taxStrategyServiceType)؛
            فار السعر = المنتج. السعر * العد ؛
            سعر العائد + taxStrategy.CalculateTax (السعر) ؛
        }
    }
}

تقوم شركة PriceCalculator بحقن IServiceProvider في مُنشئها وتعيينه إلى حقل. يستخدم PriceCalculator داخل طريقة الحساب لإنشاء نطاق خدمة تابع. يستخدم domain.ServiceProvider لحل الخدمات ، بدلاً من مثيل _serviceProvider الذي تم حقنه. وبالتالي ، يتم تلقائيًا تحرير / التخلص من جميع الخدمات التي تم حلها من النطاق في نهاية عبارة الاستخدام.

الممارسات الجيدة:

  • إذا كنت تقوم بحل خدمة في نصوص أسلوب ، فقم دائمًا بإنشاء نطاق خدمة تابع لضمان إطلاق الخدمات التي تم حلها بشكل صحيح.
  • إذا حصلت إحدى الطرق على IServiceProvider كوسيطة ، فيمكنك مباشرة حل الخدمات منها دون الاهتمام بتحرير / التخلص. إنشاء / إدارة نطاق الخدمة هي مسؤولية الكود الذي يستدعي طريقتك. باتباع هذا المبدأ يجعل كودك أكثر نظافة.
  • لا تملك إشارة إلى خدمة حلها! وإلا ، فقد يتسبب ذلك في حدوث تسرب للذاكرة وسوف تصل إلى خدمة يتم التخلص منها عند استخدام مرجع الكائن لاحقًا (ما لم تكن الخدمة التي تم حلها منفردة).

خدمات سينغلتون

خدمات Singleton مصممة عمومًا للحفاظ على حالة التطبيق. ذاكرة التخزين المؤقت مثال جيد لحالات التطبيق. مثال:

الطبقة العامة FileService
{
    ConcurrentDictionary  _cache الخاص للقراءة فقط؛
    خدمة الملفات العامة ()
    {
        _cache = جديد ConcurrentDictionary  ()؛
    }
    البايت العام [] GetFileContent (سلسلة filePath)
    {
        إرجاع _cache.GetOrAdd (filePath ، _ =>
        {
            إرجاع File.ReadAllBytes (filePath) ؛
        })؛
    }
}

يقوم FileService ببساطة بتخزين محتويات الملفات لتقليل قراءات القرص. يجب أن تكون هذه الخدمة مسجلة على شكل مفرد خلاف ذلك ، لن يعمل التخزين المؤقت كما هو متوقع.

الممارسات الجيدة:

  • إذا كانت الخدمة تحتوي على حالة ، فيجب عليها الوصول إلى هذه الحالة بطريقة آمنة للخيط. لأن جميع الطلبات تستخدم بشكل متزامن نفس مثيل الخدمة. لقد استخدمت ConcurrentDictionary بدلاً من القاموس لضمان سلامة مؤشر الترابط.
  • لا تستخدم خدمات النطاق أو العابرة من خدمات مفردة. لأنه قد لا يتم تصميم الخدمات المؤقتة لتكون آمنة لمؤشر الترابط. إذا كنت بحاجة إلى استخدامها ، فعليك الاعتناء بالخيوط المتعددة أثناء استخدام هذه الخدمات (استخدم القفل على سبيل المثال).
  • تحدث تسرب الذاكرة بشكل عام عن خدمات المفرد. لا يتم تحريرها / التخلص منها حتى نهاية التطبيق. لذلك ، إذا تم إنشاء مثيل لفئات (أو حقن) ولكن لم يتم تحريرها / التخلص منها ، فسيظلون أيضًا في الذاكرة حتى نهاية التطبيق. تأكد من إطلاق / التخلص منها في الوقت المناسب. راجع قسم حل الخدمات في قسم "طريقة الأسلوب" أعلاه.
  • إذا قمت بتخزين البيانات مؤقتًا (محتويات الملف في هذا المثال) ، يجب عليك إنشاء آلية لتحديث / إبطال البيانات المخزنة مؤقتًا عندما يتغير مصدر البيانات الأصلي (عندما يتغير ملف مؤقت في القرص على هذا المثال).

خدمات النطاق

يبدو مدى الحياة أولاً أنه مرشح جيد لتخزين بيانات طلب الويب. لأن ASP.NET Core ينشئ نطاق خدمة لكل طلب ويب. لذلك ، إذا قمت بتسجيل خدمة ما على أساس النطاق ، فيمكن مشاركتها أثناء طلب ويب. مثال:

الطبقة العامة RequestItemsService
{
    readonly Dictionary  _items؛
    RequestItemsService () طلب عام
    {
        _items = قاموس جديد  ()؛
    }
    مجموعة الفراغ العام (اسم السلسلة ، قيمة الكائن)
    {
        _items [name] = value ؛
    }
    كائن عمومي Get (اسم السلسلة)
    {
        return _items [name]؛
    }
}

إذا قمت بتسجيل RequestItemsService كما تم تحديد نطاقه وحقنه في خدمتين مختلفتين ، فيمكنك الحصول على عنصر تمت إضافته من خدمة أخرى لأنهم سيشاركون نفس مثيل RequestItemsService. هذا ما نتوقعه من خدمات النطاق.

لكن .. الحقيقة قد لا تكون دائما هكذا. إذا قمت بإنشاء نطاق خدمة تابعة وحلت خدمة RequestItemsService من نطاق تابع ، فستحصل على نسخة جديدة من RequestItemsService ولن تعمل كما تتوقع. لذلك ، لا تعني خدمة النطاق دومًا مثيل لكل طلب ويب.

قد تعتقد أنك لا ترتكب مثل هذا الخطأ الواضح (حل النطاق الموجه داخل نطاق تابع). لكن هذا ليس خطأ (استخدام منتظم للغاية) وقد لا تكون الحالة بهذه البساطة. إذا كان هناك رسم بياني كبير للاعتماد بين الخدمات الخاصة بك ، فلا يمكنك معرفة ما إذا كان أي شخص قد أنشأ نطاقًا تابعًا وحل الخدمة التي تضخ خدمة أخرى ... والتي تحقن أخيرًا خدمة نطاقية.

الممارسة الجيدة:

  • يمكن اعتبار الخدمة التي تم تحديد نطاقها على أنها تحسين حيث يتم ضخها بواسطة الكثير من الخدمات في طلب ويب. وبالتالي ، ستستخدم كل هذه الخدمات مثيلًا واحدًا للخدمة أثناء طلب الويب نفسه.
  • لا يلزم تصميم الخدمات التي يتم تحديد نطاقها لتكون آمنة للخيط. لأنه ، يجب استخدامها عادةً بواسطة طلب / خيط ويب واحد. ولكن ... في هذه الحالة ، يجب ألا تشارك نطاقات الخدمة بين مؤشرات الترابط المختلفة!
  • كن حذرًا إذا كنت تقوم بتصميم خدمة نطاق لمشاركة البيانات بين خدمات أخرى في طلب ويب (موضح أعلاه). يمكنك تخزين بيانات لكل طلب ويب داخل HttpContext (حقن IHttpContextAccessor للوصول إليها) وهي الطريقة الأكثر أمانًا للقيام بذلك. لم يتم تحديد نطاق حياة HttpContext. في الواقع ، لم يتم تسجيله لدى شركة DI على الإطلاق (لهذا السبب لم تقم بحقنه ، ولكن حقن IHttpContextAccessor بدلاً من ذلك). يستخدم تطبيق HttpContextAccessor AsyncLocal لمشاركة HttpContext نفسه أثناء طلب ويب.

خاتمة

يبدو أن استخدام تبعية الحقن سهل الاستخدام في البداية ، ولكن هناك مشاكل محتملة متعددة الخيوط وتسرب الذاكرة إذا لم تتبع بعض المبادئ الصارمة. شاركت بعض المبادئ الجيدة بناءً على تجاربي الخاصة أثناء تطوير إطار عمل ASP.NET Boilerplate.