آشنایی با معماری پیاز (Onion Architecture) در MVC

در این مقاله شما را با معماری پیاز و نحوه پیاده سازی آن آشنا خواهیم کرد

آشنایی با معماری پیاز (Onion Architecture) در MVC

اخیراً یکی از بحثهای داغ و روز دنیای برنامه نویسی اصطلاحاً "معماری پیاز" نام دارد , به همبن دلیل قصد داریم قدم به قدم با این معماری آشنا شویم .حتی با استفاده از برخی توضیحات در این مقاله شما میتوانید هسته , ساختار , و یا یک پروژه آزمایشی در نوعی دیگر از نرم افزارها مانند WCF یا WPF به کار بگیرید .

معماری پیاز Onion Architecture چیست ؟

آموزش کامل و پروژه محور معماری پیاز در mvc را در تاپ لرن میتوانید مشاهده کنید .

این معماری یکی از روش های مفید و ارجح معماری نرم افزار برای قابل آزمون بودن (Testability) , قابلیت نگهداری (maintainability) و قابلیت اطمینان (dependability) در ساختار نرم افزار مانند بانک اطلاعات یا سرویس های مختلف مورد استفاده می باشد .

مزیت های معماری پیاز :

- در این معماری لایه ها با استفاده از interface با یکدیگر ارتباط دارند ,

- هر وابستگی خارجی مانند بانک اطلاعاتی access و یا web service  به عنوان لایه خارجی شناخته می شود

- UI یا همان نمای ظاهری نیز جزئی از لایه های خارجی میباشد .

- objectهایی که معرف دامنه و حوزه میباشند به عنوان قسمتی از لایه های داخلی محسوب میشوند .

- لایه های خارجی به لایه های داخلی خود وابسته می باشند .

- لایه های داخلی نباید به لایه های بیرونی وابستگی داشته باشند .

- دامنه متغیر هایی که در هسته وجود دارند می توانند هم به UI و هم به بانک اطلاعاتی دسترسی داشته باشند

- تمامی ارتباطات به سمت هسته میباشد

- برخی کد ها ممکن است قسمتی از لایه های خارجی را تغییر دهد

ساختار پروژه

ما در 4 پروژه با این معماری کار میکنیم :

1 . (Class Library (Core Project

2 .  linfrastructure project

3 . Test Project

4 . Web Project - MVC Project

Core Project  که شامل حوزه موجودیت ها و repositories interface می باشد . ساختار پروژه شامل کلاسها میباشد که با بانک اطلاعاتی می باشد . Test Project که شامل آزمونها می باشد و MVC Project که شامل controle های مربوط به MVC میباشد .

ساختار کلی solution باید به صورت لیست باشد , مانند تصویر زیر :

هسته پروژه :

هسته پروژه که داخلی ترین لایه معماری پروژه می باشد .همه لایه های بیرونی مانند زیرساخت (infrastructure) و باقی به هسته وابستگی دارند و از آن استفاده میکنند .

اغلب هسته پروژه شامل موارد زیر می باشد :

1 - Domain Entities

2 - Repositories interfaces

همچنین هیچ وابستگی به لایه های خارجی ندارد .به عنوان مثال ما نباید از Refrence های زیر در هسته پروژه استفاده کنیم :

1 . Refrence مربوط به ORM مانند LINQ to SQL

2 . Refrence به کتابخانه ADO.NET

3 . Refrence به Entity Framework

ایجاد Entity :

در ادامه یک کلاس entity با نام "BloodDonor" ایجاد می کنیم :


    public class BloodDonor  
    {  
        public string BloodDonorID { get; set; }  
        public string Name { get; set; }  
        public DateTime Dob { get; set; }  
        public string BloodGroup { get; set; }  
        public string City { get; set; }  
        public string Country { get; set; }  
        public int PinCode { get; set; }  
        public string PhoneNumber { get; set; }  
        public string Email { get; set; }  
        public bool IsActive { get; set; }  
        public bool IsPrivate { get; set; }  
        public bool IsVerified { get; set; }  
    }  

ممکن است با برخی ملزومات مربوط به امکانات entity  مواجه شویم , به عنوان مثال : حد اکثر طول و غیره . برای رفع این ملزومات ما 2 روش در اختیار داریم :

1 - استفاده از کتابخانه System.ComponentModel.DataAnnotations

2 - استفاده از Entity Framework

هر دو روش ذکر شده هر کدام مزیت های خاص خود را دارا می باشند , برای رهایی از وابستگی به لایه های خارجی مانند EntityFrameword پیشنهاد میکنیم که از DataAnnotation ها استفاده کنید .

برای استفاده از DataAnnotation ها باید فضای نام System.componentModel.DataAnnotation را به هسته اضافه کنیم , ما میتوانیم کلاس entity را مانند زیر تغییر دهیم  :


    using System;  
    using System.ComponentModel.DataAnnotations;  
      
    namespace LifeLine.Core  
    {  
        public class BloodDonor  
        {  
            [Required]  
            public string BloodDonorID { get; set; }  
            [Required]  
            [MaxLength(50)]  
            public string Name { get; set; }  
            [Required]  
            public string BloodGroup { get; set; }  
            [Required]  
            public string City { get; set; }  
      
            public string Country { get; set; }  
            [Required]  
            public int PinCode { get; set; }  
            [Required]  
            public string PhoneNumber { get; set; }  
            public string Email { get; set; }  
            public bool IsActive { get; set; }  
            public bool IsPrivate { get; set; }  
            public bool IsVerified { get; set; }  
        }  
    }  

اکنون یک کلاس entity با نام "BloodDonor"  به همرا ه data annotation ایجاد کرده ایم .قدم بعدی باید repository interface را اضافه کنیم .

ایجاد Repository interface :

در ادامه می توانید الگوی repository را مشاهده نمایید , تقریباً این الگو ما را قادر میسازد کلاس دسترسی به بانک اطلاعاتی را replace کنیم .


    using System.Collections.Generic;  
    namespace LifeLine.Core.Interfaces  
    {  
        public interface IBloodDonorRepository   
        {  
            void Add(BloodDonor b);  
            void Edit(BloodDonor b);  
            void Remove(string BloodDonorID);  
            IEnumerable < BloodDonor > GetBloodDonors();  
            BloodDonor FindById(string BloodDonorID);  
      
        }  
    }  

اکنون ما domain entity را ایجاد کرده ایم .قدم بعدی ایجاد infrastructure project می باشد .

Infrastructure Project :

در این قسمت ما عملیاتی که به خارج از نرم افزار اتصال دارد را ایجاد میکنیم , به عنوان مثال :

1 - عملیات بانک اطلاعاتی Database Operation

2 - دسترسی به سرویسهای خارجی Accessing Outside Service

3 - دسترسی به فایل های سیستم Accessing File System

عملیات های ذکر شده در بالا باید قسمتی از infrastructure project باشند .همچنین از Entity Framework  برای انجام عملیات بانک اطلاعاتی استفاده میکنیم .ما از روش CodeFirst  استفاده میکنیم .بنابراین باید از procedure استفاده کنیم :

1 - کلاس data context را ایجاد می کنیم .

2 - کلاس repository را به پروژه اضافه میکنیم .

3 - کلاس database initializer را ایجاد کنیم .

برای اضافه کردن refrence بر روی infrastructure project راست کلیک میکنیم و Nuget Package را انتخاب میکنیم و در پنجره باز شده Entity Framework Package را انتخاب میکنیم .

DataContext class :

ابتدا کلاس data context را ایجاد میکنیم .یک جدول با نام "BloodDonors" ایجاد میکنیم :


    using System.Data.Entity;  
    using LifeLine.Core;  
      
    namespace LifeLine.Infrastructure   
    {  
        public class BloodDonorContext: DbContext  
        {  
            public BloodDonorContext(): base("name=BloodDonorContextConnectionString")   
            {  
                var a = Database.Connection.ConnectionString;  
            }  
      
            public DbSet < BloodDonor > BloodDonors{get;set;}  
        }  
    }  

Connection string :

ما باید connection string  را در app.config مربوط به infrastructure project تنظیم میکند .

< connectionStrings >   
< add name = "BloodDonorContextConnectionString" connectionString = "Data Source= LocalDb)\v11.0;Initial Catalog=BloodDonors;Integrated Security=True;MultipleActiveResultSets=true" providerName = "System.Data.SqlClient" / >   
< /connectionStrings> 

Database initialize class :

در این قسمت ما میخواهیم تعدادی مقادیر اولیه به صورت پیش فرض تعریف کنیم .مانند زیر :

using System;  
using System.Data.Entity;  
using LifeLine.Core;  
  
namespace LifeLine.Infrastructure  
{  
    public class BloodDonorInitalizeDb: DropCreateDatabaseIfModelChanges < BloodDonorContext > {  
  
        protected override void Seed(BloodDonorContext context)   
        {  
            context.BloodDonors.Add(  
            new BloodDonor   
            {  
                Name = "Rahul Kapoor",  
                City = "Gurgaon",  
                BloodGroup = "A+",  
                BloodDonorID = "BD1",  
                Country = "India",  
                IsActive = true,  
                IsPrivate = false,  
                IsVerified = true,  
                PhoneNumber = "91+7378388383",  
                PinCode = 122002,  
                Email = "Rahul@abc.com"  
  
            });  
            context.BloodDonors.Add(  
            new BloodDonor  
            {  
                Name = "Salman Khan",  
                City = "Mumbai",  
                BloodGroup = "A-",  
                BloodDonorID = "BD2",  
                Country = "India",  
                IsActive = true,  
                IsPrivate = false,  
                IsVerified = true,  
                PhoneNumber = "91+84848484",  
                PinCode = 25678,  
                Email = "Salman@abc.com"  
            });  
            base.Seed(context);  
        }  
    }  
} 

اکنون ما مقادیر داخل جدول را تعریف کرده ایم .اگر model تغییر کند , مجدداً به بانک اطلاعاتی متصل و مقادیر را میخواند .

Repository class implementation :

در ادامه ما احتیاج به تعریف کلاس repository داریم .repository class به بانک اطلاعاتی دسترسی دارد و از تکنولوژی LINQ to Entity استفاده می کند .برای ایجاد کلاس BloodDonorRepository باید کلاسی ایجاد کنیم که BloodDonorRepository interface را تعریف کنیم .


    using System.Collections.Generic;  
    using System.Linq;  
    using LifeLine.Core;  
    using LifeLine.Core.Interfaces;  
      
    namespace LifeLine.Infrastructure  
    {  
        public class BloodDonorRepository: IBloodDonorRepository {  
            BloodDonorContext context = new BloodDonorContext();  
            public void Add(BloodDonor b)   
            {  
                context.BloodDonors.Add(b);  
                context.SaveChanges();  
            }  
      
            public void Edit(BloodDonor b)   
            {  
                context.Entry(b).State = System.Data.Entity.EntityState.Modified;  
            }  
      
            public void Remove(string BloodDonorID)   
            {  
                BloodDonor b = context.BloodDonors.Find(BloodDonorID);  
                context.BloodDonors.Remove(b);  
                context.SaveChanges();  
            }  
      
            public IEnumerable < BloodDonor > GetBloodDonors()   
            {  
                return context.BloodDonors;  
            }  
      
            public BloodDonor FindById(string BloodDonorID)   
            {  
                var bloodDonor = (from r in context.BloodDonors where r.BloodDonorID == BloodDonorID select r).FirstOrDefault();  
                return bloodDonor;  
            }  
        }  
    }  

برای اجرای repository class ما از LINQ to Entity استفاده کرده ایم .به عنوان مثال برای افزودن blood donor :

1 یک شئی از context class ایجاد میکنیم .

2 context.entity.add را بکار می بریم .

3 ()context.savechanges را در آخر اضافه میکنیم .

بعد از ایجاد هسته پروژه و infrastructure ما باید تصمیم بگیریم که مستقیماً MVC Project را ایجاد کنیم یا اینکه کلاس repository را برای unit test را ایجاد کنیم .روش بهتر این است که Unit Test را برای repository class ایجاد کنیم که در این حالت ما را از کد های مربوط به بانک اطلاعاتی به درستی اجرا میشوند .

Test Project :

اکنون ما test project  را با انتخاب unit test از قسمت Test Project ایجاد کرده ایم .برای شروع نوشتن test , در ابندا باید refrence های زیر را اضافه کنیم :

1 . core project

2 . infrastructure project

3 . entity framework package

4 . system.LINQ

در test Project ما کلاسی با نام BloodDonorRepositoryTest ایجاد می کنیم .در داخل کلاس unit test  می بایست test را مقدار دهی اولیه کنیم .مانند لیست زیر :


    BloodDonorRepository repo;  
    [TestInitialize]  
    public void TestSetUp()   
    {  
      
        BloodDonorInitalizeDb db = new BloodDonorInitalizeDb();  
        System.Data.Entity.Database.SetInitializer(db);  
        repo = new BloodDonorRepository();  
    }  

در تنظیمات test ما یک instance از کلاس BloodDonorInitializeDB ایجاد میکنیم .سپس بانک اطلاعاتی را به صورت پیش فرض مقدار دهی اولیه میکنیم .بنابراین در تنظیمات test , شئی instance از کلاس repository ایجاد میشود .در ادامه test را برای اعتبارسنجی ایجاد میکنیم .

[TestMethod]  
public void IsRepositoryInitalizeWithValidNumberOfData()   
{  
    var result = repo.GetBloodDonors();  
    Assert.IsNotNull(result);  
    var numberOfRecords = result.ToList().Count;  
    Assert.AreEqual(2, numberOfRecords);  
} 

در بالا متد  GetBloodDonors را فراخوانی میکنیم و سپس تعداد رکوردهای بانک اطلاعاتی را مورد بررسی میکنیم .در یک اتصال دلخواه test که در بالا تعریف کرده ایم باید پاس داده شود .شما میتوانید نتیجه را در Test-Window-Test مشاهده و بررسی کنید .

web Project :

ما باید MVC Project را ایجاد کنیم که از هسته پروژه و infrastructure استفاده میکند .در ادامه refrence هایی که مورد استفاده قرار میگیرد را ذکر میکنیم :

1 - infrastructure project

2 - core project

3 - entity framework package

بعد از افزودن refrence ها یکبار پروژه را build میکنیم .اگر پروژه با خطا مواجه نشد و پیغام build succesful می باشد , در قسمت controller راست کلیک میکنیم , یک controller جدید ایجاد میکنیم سپس نام controller جدید را به BloodDonorsController تغییر میدهیم .

همچنین تنظیمات را مانند مراحل زیر انجام میدهیم :

با استفاده از scaffolding مشاده می کنیم که controller به همراه view ایجاد میشود .پوشه BloodDonors در داخل پوشه Views اضافه میشود , مانند تصویر زیر :

در ادامه باید یک instance از کلاس BloodDonorInitializeDB ایجاد کنیم .مانند کد زیر :

BloodDonorInitalizeDb db = new BloodDonorInitalizeDb();  
System.Data.Entity.Database.SetInitializer(db);

آخرین قسمتی که ما احتیاج داریم connection String را از app.config در داخل infrastructure project کپی و در web.config قرار میدهیم .


    <connectionStrings>  
    <add name="BloodDonorContextConnectionString" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=BloodDonors;Integrated Security=True;MultipleActiveResultSets=true" providerName="System.Data.SqlClient"/>  
    </connectionStrings>  

در این قسمت اگر تمام مراحل را به درستی انجام داده باشیم و پروژه را اجرا کنیم باید در خروجی به قسمت عملیات CRUD هدایت شویم .

وابستگی injection با استفاده از unity :

ما پروژه را اجرا میکنیم , اما با پیغام خطا مواجه می شویم .با توجه به کلاس های مربوط به controller ما باید object مورد نظر از data context را در controller ایجاد شده به صورت مستقیم می توانیم پیدا کنیم . اکنون controller و data context با یکدیگر همراه شده اند و اگر ما بانک اطلاعاتی را تغییر دهیم controller نیز تحت تاثیر قرار میگردد.ما نباید BloodDonorContext را داخل controller داشته باشیم :

ما اکنون کلاس repository را داخل infrastructure project ایجاد کرده ایم .ما باید از کلاس repository در داخل controller استفاده کنیم .در ادامه دو انتخاب برای استفاده از repository class داریم :

1 -مستقیماً یک object از کلاس repository در داخل کلاس controller class ایجاد میکنیم .

2 - dependency injection را برای repository interface به منظور تمرکز repository class در زمان اجرا با استفاده از Unity container استفاده میکنیم .

ما از روش دوم استفاده میکنیم .برای استفاده از unity container باید Unity.Mvc5 package را  از Manage Nuget Package به پروژه اضافه کنیم .

بعد از نصب Unity Package ,  ما باید نوع مورد نظر را ثبت کنیم .برای ثبت نوع مورد نظر UnityConfig را  در داخل پوشه App-Start باز می کنیم  .

و در داخل global.aspx تمام کامپوننت های UnityConfig ثبت میکنیم .

ما باید Unitiy Container refrence را نیز اضافه کنیم و نوع آن را ثبت کنیم .در ادامه از refactor برای کلاس container استفاده میکنیم .این عمل برای

1 - یک Global Variable از نوع BloodDonorRepository ایجاد میکنیم .

2 - یک controller class constructor با پارامترهایی که در زیر نمایش داده شده ایجاد میکینم .

یک مرتبه این عمل که انجام شد ، ما باید از refactor برای کلاس controller برای استفاده از این تابع از کلاس repository در عوض کلاس data context استفاده کنیم .

در ادامه لیست کامل کد کلاس BloodDonorcontroller به صورت زیر میباشد :

using System.Linq;  
using System.Net;  
using System.Web.Mvc;  
using LifeLine.Core;  
using LifeLine.Core.Interfaces;  
namespace LifeLine.Web.Controllers   
{  
    public class BloodDonorsController: Controller   
    {  
        IBloodDonorRepository db;  
  
        public BloodDonorsController(IBloodDonorRepository db)   
        {  
            this.db = db;  
        }  
  
        // GET: BloodDonors  
        public ActionResult Index()   
        {  
            return View(db.GetBloodDonors().ToList());  
        }  
  
        // GET: BloodDonors/Details/5  
        public ActionResult Details(string id)   
        {  
            if (id == null)  
            {  
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);  
            }  
            BloodDonor bloodDonor = db.FindById(id);  
            if (bloodDonor == null)   
            {  
                return HttpNotFound();  
            }  
            return View(bloodDonor);  
        }  
  
        // GET: BloodDonors/Create  
        public ActionResult Create()   
        {  
            return View();  
        }  
  
        // POST: BloodDonors/Create  
        // To protect from overposting attacks, please enable the specific properties you want to bind to, for   
        // more details see http://go.microsoft.com/fwlink/?LinkId=317598.  
        [HttpPost]  
        [ValidateAntiForgeryToken]  
        public ActionResult Create([Bind(Include = "BloodDonorID,Name,BloodGroup,City,Country,PinCode,PhoneNumber,Email,IsActive,IsPrivate,IsVerified")] BloodDonor bloodDonor)   
        {  
            if (ModelState.IsValid)  
            {  
                db.Add(bloodDonor);  
                return RedirectToAction("Index");  
            }  
  
            return View(bloodDonor);  
        }  
  
        // GET: BloodDonors/Edit/5  
        public ActionResult Edit(string id)   
        {  
            if (id == null)   
            {  
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);  
            }  
            BloodDonor bloodDonor = db.FindById(id);  
            if (bloodDonor == null)   
            {  
                return HttpNotFound();  
            }  
            return View(bloodDonor);  
        }  
  
        // POST: BloodDonors/Edit/5  
        // To protect from overposting attacks, please enable the specific properties you want to bind to, for   
        // more details see http://go.microsoft.com/fwlink/?LinkId=317598.  
        [HttpPost]  
        [ValidateAntiForgeryToken]  
        public ActionResult Edit([Bind(Include = "BloodDonorID,Name,BloodGroup,City,Country,PinCode,PhoneNumber,Email,IsActive,IsPrivate,IsVerified")] BloodDonor bloodDonor)   
        {  
            if (ModelState.IsValid)   
            {  
                db.Edit(bloodDonor);  
                return RedirectToAction("Index");  
            }  
            return View(bloodDonor);  
        }  
  
        // GET: BloodDonors/Delete/5  
        public ActionResult Delete(string id)   
        {  
            if (id == null)  
            {  
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);  
            }  
            BloodDonor bloodDonor = db.FindById(id);  
            if (bloodDonor == null)   
            {  
                return HttpNotFound();  
            }  
            return View(bloodDonor);  
        }  
  
        // POST: BloodDonors/Delete/5  
        [HttpPost, ActionName("Delete")]  
        [ValidateAntiForgeryToken]  
        public ActionResult DeleteConfirmed(string id)  
        {  
            BloodDonor bloodDonor = db.FindById(id);  
            db.Remove(bloodDonor.BloodDonorID);  
            return RedirectToAction("Index");  
        }  
    }  
}