مقدمة الى Dependency Injection

-

Dependency Injection (DI) هو تقنية في ASP.NET Core MVC لتحقيق  loosely coupling بين objects. إذا كانت Controller تعتمد على class آخر، فعندئذٍ يتم تحديد dependency  في هذا ال class في constructor التابع ل Controller. وعند وجود هذه dependency ، يوفر Core MVC ال class object إلى Controller تلقائيًا. لذلك يوفر هذا ما يسمى loosely coupling  بين Controller وال class الأخرى

تمام وحتي نفهم الموضوع نكمل شغل على مشروعنا السابق StudentsAcademy 
اولاً بنحتاج الى انشاء Model باسم CoursesModel مشان هيك ننتقل الى المجلد Models  في المشروع ونضيف class باسم CoursesModel.cs  كما هو موضحة أدناه:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace StudentsAcademy.Models
{
    public class CoursesModel
    {
        public string CourseNumber { get; set; }
        public string CourseName { get; set; }
        public string CourseDescription { get; set; }
        public decimal Price { get; set; }
        public int Capacity { get; set; }
    }
}
تمام الان بنحتاج إنشاء interface للتعامل مع المواد الدراسية لذلك قم بإنشاء مجلد جديد باسم Interface ومن ثم قم بإضافة interface بالنقر يمين فوق مجلد interface واختر من القائمة add- new item ثم ابحث عن interface وحدد الاسم ICourses  لهذه ال interface. لاحظ الصورة في الأسفل. 



أضف الكود التالي الى هذه interface 

using StudentsAcademy.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace StudentsAcademy.Interface
{
    interface ICourses
    {
        IEnumerable<CoursesModel> _CoursesModel { get; }
        CoursesModel this[string name] { get; }
        void AddCourse(CoursesModel _CoursesModel);
        void DeleteCourse(int CourseId);
    }
}
سيتم تنفيذ هذه interface في class أخر يسمى CourseRepository. لذا قم بإنشاء مجلد اخر باسم Repository ومن ثم انشي ملف class باسم CourseRepository.cs داخل مجلد Repository لاحظ الصورة في الاسفل:


ثم أضف الكود التالي: 

using StudentsAcademy.Interface;
using StudentsAcademy.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace StudentsAcademy.Repository
{
    public class CourseRepository : ICourses
    {
        private Dictionary<string, CoursesModel> _coursesModel;
        public CourseRepository()
        {
            _coursesModel = new Dictionary<string, CoursesModel>();
            new List<CoursesModel> {
                new CoursesModel { CourseNumber = "0001",CourseName="Math",CourseDescription="Math Desc",Capacity=20, Price = 115 },
                new CoursesModel { CourseNumber = "0002",CourseName="Sciences",CourseDescription="Sciences Desc",Capacity=30, Price = 120 },
                new CoursesModel { CourseNumber = "0003",CourseName="Physics",CourseDescription="Physics Desc",Capacity=25, Price = 140 }
            }.ForEach(p => AddCourse(p));
        }
        public IEnumerable<CoursesModel> _CoursesModel => _coursesModel.Values;
        public CoursesModel this[string name] => _coursesModel[name];
        public void AddCourse(CoursesModel Courses) => _coursesModel[Courses.CourseName] = Courses;
        public void DeleteCourse(CoursesModel Courses) => _coursesModel.Remove(Courses.CourseNumber);
    }
}
في هذه المرحلة سنقوم بالتعامل مع بيانات مؤقتة يتم تخزينها في الذاكرة باستخدام Dictionary. بعد فهم ذلك سنقوم بالتعامل مع قاعدة البيانات ومن ثم اجراء بعص العمليات، تمام
انشاء البيانات المؤقتة هنا يتم بواسطة constructor   ل class Repository الذي يقوم بتهيئة dictionary جديد للمواد وإضافة 3 منتجات إلى هذا القاموس. والكود الي بيعمل هذا الاشي:


        {
            _coursesModel = new Dictionary<string, CoursesModel>();
            new List<CoursesModel> {
                new CoursesModel { CourseNumber = "0001",CourseName="Math",CourseDescription="Math Desc",Capacity=20, Price = 115 },
                new CoursesModel { CourseNumber = "0002",CourseName="Sciences",CourseDescription="Sciences Desc",Capacity=30, Price = 120 },
                new CoursesModel { CourseNumber = "0003",CourseName="Physics",CourseDescription="Physics Desc",Capacity=25, Price = 140 }
            }.ForEach(p => AddCourse(p));
        }

باقي الكود هو تمثيل لجميع العمليات الموجودة داخل interface، وبنعمل هون عمليات الاضافة والحذف وعرض المواد
الإعدادات Configuration
في MVC Core اجباري نعمل بعض الاعدادات حتي بشتغل المشروع معنا بدون مشاكل :
طيب حتي نعمل هذه الاعدادات ننتقل الى ملف Startup.cs ونتأكد من  اضافة services و configurations الضرورية داخل Function التالية:
()ConfigureServices() and Configure  وبهذه المرحلة بنكتفي باضافة الكود التالي:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using StudentsAcademy.Services;
using StudentsAcademy.Middleware;
using StudentsAcademy.Share;
using Microsoft.EntityFrameworkCore;
namespace StudentsAcademy
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
        public IConfiguration Configuration { get; }
        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<Connections>(Configuration.GetSection("ConnectionStrings"));
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
            services.AddSingleton<TotalStudents>();
            services.AddControllersWithViews();
        }
        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseStatusCodePages();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }
            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseRouting();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

طيب نوخذ فكره سريعه عن الكود المكتوب:
عرفنا في البداية متغير من النوع IConfiguration وهذا يستخدم للوصول الى ملفات الاعدادت في التطبيق. 
في function المسى ConfigureServices استقبلنا متغير من نوع IServiceCollection  لاضافة الخدمات الخاصة بالتطبيق، وفي هذا الكود اضفنا الاتصال بقاعدة البيانات الي بتكون مخزنة في ملف الاعدادات.
استخدمنا ايضا الامر AddSingleton لاضافة الخدمات في التطبيق وهنا استخدمنا TotalStudents. واكيد لازم اضيف الامر 
services.AddControllersWithViews();
الي بيعني ان هذا التطبيق بيستخدم Controllers and Views 

وفي ال function المسمى Configure استخدمنا مجموعة من الاوامر لاعداد التطبيق لاستخدامها مثل : 

if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseStatusCodePages();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }
في هذا الكود بدأنا بفحص وضع التطبيق اذا كان في مرحلة التطوير ()env.IsDevelopment (لاحظ ان المتغير env من نوع IWebHostEnvironment) فهذا بيعني استخدام صفحة تفاصيل الخطأ ()app.UseDeveloperExceptionPage وهذا الصفحة تستخدم مع المبرمجين لمعرفة تفاصيل الخطأ، اذا كانت في وضع Release يعني النسخة النهائية بنستخدم صفحة الخطأ باستخدام  الامر 
 app.UseExceptionHandler("/Home/Error");
وغالبا هذه الصفحة بنجهزها لعرضها للمستخدم النهائي بدل صفحة الخطأ بالتفصيل (ممكن تكتب فيها نعذتر عن هذا الخطأ جاري العمل على حل المشكلة واكيد لازم تكون في Back-end عامل Code لمعرفة الخطأ وتسجيله مثلا في قاعدة البيانات او ارسال ايميل بالخطأ حتي تعرف ان في مشكلة). 
بقية الاوامر هي اعددات للتطبيق مثلا بنعرف عليها بالتفصيل اثناء الدروس القادمه.

تمام بعد ما عرفنا الاعدادات وجهزنها Model و Interface الان دور نعدل على Controller الي من خلالو بنعمل استقبال للبيانات وعرضها. طيب ننتقل الى مجلد Controllers   ونضيف controller  جديدة تسمى CoursesController ثم نضيف الكود التالي:

using Microsoft.AspNetCore.Mvc;
using StudentsAcademy.Repository;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace StudentsAcademy.Controllers
{
    public class CoursesController : Controller
    {
        public IActionResult Index()
        {
            return View(new CourseRepository()._CoursesModel);
        }
    }
}

طيب، واضح جدا في هذا الكود ان في Index Action Method  عملنا object من نوع Repository class واستدعاء CoursesModel property باستخدام الكود :
new CourseRepository()._CoursesModel
تذكير CoursesModel_ هي Dictionary في CourseRepository تحتوي على جميع المواد الدراسية.
بمعني آخر عملنا استدعاء لجميع المواد الدراسية. ثم أخيرًا عملنا استدعاء ل View لعرض المواد الدراسية.

اضافة View

تأكد من اضافة View تابع لهذه Controller، لإضافة View انتقل الى ملف CoursesController ومن ثم داخل Index Action  انقر يمين كما في الصورة


ثم اختر razor view  



ثم انقر فوق Add (سيقوم visual studio بإنشاء view بنفس اسم controller و action) في هذ الحالة سيتم انشاء مجلد باسم Courses مع view باسم Index كما في الصورة في الأسفل



أضف الكود التالي الى View :

@model IEnumerable<CoursesModel>
@{ Layout = null; }
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Dependency Injection</title>
    <link rel="stylesheet" asp-href-include="lib/twitter-bootstrap/css/bootstrap.css" />
</head>
<body class="m-1 p-1">
    @if (ViewData.Count > 0)
    {
        <table class="table table-bordered table-sm table-striped">
            @foreach (var kvp in ViewData)
            {
                <tr><td>@kvp.Key</td><td>@kvp.Value</td></tr>
            }
        </table>
    }
    <table class="table table-bordered table-sm table-striped">
        <thead>
            <tr>
                <th>Course Number</th>
                <th>CourseName</th>
                <th>CourseDescription</th>
                <th>Price</th>
                <th>Capacity</th>
            </tr>
        </thead>
        <tbody>
            @if (Model == null)
            {
                <tr><td colspan="3" class="text-center">No Model Data</td></tr>
            }
            else
            {
                @foreach (var p in Model)
                {
        <tr>
            <td>@p.CourseNumber</td>
            <td>@p.CourseName</td>
            <td>@p.CourseDescription</td>
            <td>@string.Format("{0:C2}", p.Price)</td>
            <td>@p.Capacity</td>
        </tr>
                }
            }
        </tbody>
    </table>
</body>
</html>
تأخذ View هذه model من النوع IEnumerable حيث يقوم Index action  بإرجاع جميع المواد الموجودة في  CoursesModel_ على شكل IEnumerable 

تشغيل التطبيق 
خلونا الان نشغل التطبيق ونشوف النتيجة:
https://localhost:44382/Courses
واكيد النتيجة بتكون 


لاحظ هنا، التصميم غير متناسق والسبب في ذلك بسبب عدم استخدام حزم Bootstrap 

تثبيت Install Bootstrap Package

لتغيير شكل الصفحات في التطبيق تحتاج الى استخدام حزمة Bootstrap لذلك قم بتثبيت حزمة Bootstrap عن طريق النقر فوق Tools – NuGet Package Manager – Mange NuGet Package for solution كما في الصورة


 

ثم انتقل الى تبويب Browse  وابحث عن Bootstrap  ثم قم بتثبيتها بالنثر فوق install كما في الصورة أسفل 


تأكد من تثبيت حزمة Bootstrap داخل مجلد wwwroot ➤ lib 

ViewImports.cshtml_
داخل مجلد Views تأكد من إضافة ملفViewImports.cshtml _.
لإضافة ملف ViewImports انقر يمين فوق مجلد views ثم اختر add-Razor Component  ثم ابحث عن ViewImports كما في الصورة.



لقد قمنا بالفعل بإضافة هذا الملف من قبل. لذلك سيكون الاسم المقترح في شاشة الإضافة ViewImports1.cshtml_ لذلك تأكد من عدم إضافة ملف آخر.
أضف الكود التالي الى ملف ViewImports

@using StudentsAcademy
@using StudentsAcademy.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

الغرض من ملف ViewImports.cshtml_ هو اضافة namespaces في مكان واحد بحيث يمكن استخدامها في جميع views الأخرى.وايضا اضافة TagHelpers التي سنستخدمها في التطبيق ASP.NET Core.
للمزيد حول ال ViewImports انتقل الى الدرس الخاص بها من خلال الرابط 
بالرجوع الى view اضفنا الكود التالي :
<link rel="stylesheet" asp-href-include="lib/twitter-bootstrap/css/bootstrap.css" />
هذا الامر مسؤول عى استدعاء ملف bootstrap.css والي هو المسؤول عن تصميم الصفحه.

شغل التطبيق الان بالدخول على الرابط :

طريقة كتابة الكود هنا في Controllers ، تمت بما يعرف ب مقترنة بإحكام  Tightly Coupled   مع  Repository class. السبب هو إنشاء object من Repository class من أجل إرجاع المواد إلى View باستخدام الكود:
return View(new CourseRepository()._CoursesModel); 
تعتبر البرمجة بطريقة Tightly Coupled طريقة برمجة سيئة bad programming practices لأنها تسبب:
• 1. مشاكل في صيانة المشروع - لأنه إذا تغير أحد المكونات فسيؤثر على المكون الآخر أيضًا.
• 2. مشاكل في أداء اختبار Unit Testing للمكونات.
افترض بعد مرور بعض الوقت أنك بحاجة إلى اجراء بعض التعديلات لإظهار بعض المواد الأخرى المدرجة في class آخر (مثل New courses). في هذه الحالة ، يتعين عليك إجراء تغييرات على index في controller  مثل هذا:
public IActionResult Index()
{
    return View(new NewRepository().Courses);
}
تمام، تعتبر أفضل الممارسات في البرمجة هنا هي استخدام حقن التبعية , Dependency Injection (DI) ولتطبيق ذلك وفي هذا المثال يجب ان لا يكون ل class المسمى Home Comptroller أي معرفة حول Class المسمى Courses التي يجب استخدامها أو كيفية إنشاء class. وسيتم تحقيق ذلك من خلال حقن التبعية (DI).

تمام التمام 

تنفيذ Dependency Injection في Controllers
في ASP.NET Core يمكن تطبيق حقن التبعية (DI) بسهولة للغاية. سنتعلم في هذا القسم كيفية تطبيق DI في ASP.NET Core Controllers.
طيب وللقيام بذلك ، يتم تنفيذ Repository Class  من خلال Interface بحيث يكون عملها هو فصل النظام decouple the system. ثم نعمل على استخدام هذه Interface (بدلاً من Repository class) في Home Controller.
تم القيام بذلك بالفعل هنا حيث انشاءنا Interface وقمنا بتعريف مجموعة من functions داخلها لعرض المواد والاضافة والحذف، وسميناها ICourses interface.
ملاحظة : يجب ان تكون interface هنا لها خاصية public

الكود بكون بهاذ الشكل :
الكود بكون بهاذ الشكل :
public class CourseRepository : ICourses 
{
}

تمام، بعد تعريف Interface  في شغلتين لازم نعملهم:

• 1. أضافة متغيرً من نوع Interface الى هو في هذه الحالة (ICourses) في controller في التطبيق.

• 2. أضافة constructor يحتوي على parameter لهذه interface. داخل constructor ، يمكن تعيين قيمة interface variable على قيمة parameter.

الآن في action method ، يمكنك الوصول إلى المواد باستخدام interface نفسها.

شكل الكود بعد التعديل:


using Microsoft.AspNetCore.Mvc;
using StudentsAcademy.Interface;
using StudentsAcademy.Repository;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace StudentsAcademy.Controllers
{
    public class CoursesController : Controller
    {
        private ICourses CoursesRepository;
        public CoursesController(ICourses repo)
        {
            CoursesRepository = repo;
        }
        public IActionResult Index()
        {
            return View(CoursesRepository._CoursesModel);
        }
    }
}

قبل اختبار التطبيق لازم نعمل بعض الاعدادات في التطبيق. وذلك بإضافة الخدمات الى Startup class وداخل طريقة() ConfigureServices ، أضف سطر الكود الذي تم تمييزه أدناه:
public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<Connections>(Configuration.GetSection("ConnectionStrings"));
            services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
            services.AddTransient<ICourses, CourseRepository>();
            services.AddSingleton<TotalStudents>();
            services.AddControllersWithViews();
        }

استخدمنا طريقة ()AddTransient  التي تخبر مزود الخدمة بكيفية التعامل مع dependency ، اول Parameter هو interface (هنا ICourses) بينما Parameter الثاني هو class التنفيذ (هنا CourseRepository).
تشغيل التطبيق 
شغل التطبيق وشوف النتيجة اكيد لازم تكون نفسها، ولكن الفرق هنا استخدمنا Dependency Injection ل لعمل Coupling إقران بين وحدة التحكمController و class المسى CourseRepository بشكل lightly.
ما يميز هذه الطريقة هو سهولة الصيانة، في هذا المثال اذا حصل وان تم أي تغير في المستقبل فقط تحتاج الى اجراء التعديل على service فقط في ملف Startup class
services.AddTransient<IRepository, NewRepository>();.

ميزة DI(Dependency Injection)

مع DI ، من السهل جدًا إجراء التغييرات. الكود التالي  

services.AddTransient<IRepository, NewRepository>(); 

بهذه نكون قد طبقنا مثال عملي على Dependency Injection