پیاده سازی الگوی Unit Of Work و Repository در MVC

شنبه 22 اسفند 1394

اگر یک برنامه ای داشته باشید که آنرا چند لایه تهیه کرده باشید نباید در هر لایه از آن از DbContext استفاده کنید .بلکه فقط یک بار از کلاس DbContext نمونه ایجاد می کنیم .برای حل این مشکل الگویی به نام الگوی Unit Of Work طراحی شده است که در این در لایه های مختلف برنامه یک unit of work به اشتراک گذاشته می شود .

پیاده سازی الگوی Unit Of Work و Repository در MVC

اگر یک برنامه ای داشته باشید که آنرا چند لایه تهیه کرده باشید نباید در هر لایه از آن از DbContext استفاده کنید .بلکه فقط یک بار از کلاس DbContext نمونه ایجاد می کنیم .برای حل این مشکل الگویی به نام الگوی Unit Of Work طراحی شده است که در این در لایه های مختلف برنامه یک unit of work به اشتراک گذاشته می شود .

ترجمه لفظی الگوی طراحی Unit of work الگوی طراحی واحد کار است .در این الگو تمام درخواست ها در یک صف قرار می گیرند و با پایان یافتن تمام کارها بانک به روز خواهد شد و دستور SaveChanges اجرا خواهد شد .
این روش مزیت های بسیاری دارد که یک نمونه از آن این است که کانکشنی که به بانک باز می شود کمترین تعداد را دارد و با همان کانکشن باز کلیه عملیات انجام می گیرد .مزیت دیگر این است که زمانی که از یک الگوی واحد کار استفاده می کنیم امکان بررسی خودکار تغییرات انجام شده بر روی یک Entity در لایه های مختلف به راحتی امکان پذیر است .الگوی طراحی واحد کار به صورت خودکار از مفهوم Transaction استفاده می کند و اگر در حین فراخوانی متد SaveChanges مشکلی پیش بیاید کل عملیات Rollback خواهد شد و تغییری در بانک اطلاعاتی رخ نخواهد داد .پس یک تراکنش به ازای چند عمل داریم و نه یک تراکنش به ازای هر عمل .

DbContext در EF Code first بر اساس الگوی واحد کار تهیه شده است،اما برای اینکه تمام لایه ها به dbcontext دسترسی داشته باشند لازم است یک لایه انتزاعی برای dbcontext ایجاد کنیم .این لایه را به کمک Dependency Injection در صورت نیاز ، آن را به کلاسی که قصد استفاده از آن را دارد تزریق کنیم .برای ایجاد این لایه انتزاعی که یک لایه ایی مابین دو لایه BAL و DAL است از هر دو الگوهای repository و Unit Of work استفاده می کنیم .

در این مقاله برای هر موجودیت کلاس repository را پیاده خواهیم کرد .مثلا برای موجودیت دانشجو هم کلاس Repository ایجاد خواهیم کرد و هم اینترفیس repository را.

زمانی که از کلاس repository در داخل کنترلر نمونه ایجاد می کنید این نمونه را به جای اینکه از کلاس repository ایجاد کنید از اینترفیس آن ایجاد خواهید کرد ، به این ترتیب کنترلر شما می تواند هر شی ایی که این اینترفیس را پیاده سازی کرده باشد را بپذیرد.زمانی که کنترلر اجرا می شود یک repository که با Entity Framework کار می کند دریافت خواهد کرد .زمانی که این کنترلر تحت تست واحد یا unit test اجرا می شود یک repository که با داده های ذخیره شده کار می کند دریافت می کند .

کلاس unit of work کار Repository  های چند گانه را هماهنگ می کند .این کلاس فقط یک نمونه از کلاس database context ایجاد می کند و آنرا بین repository های مختلف به اشتراک می گذارد .

اگر می خواهید که قادر باشید تا unit testing را به صورت مجزا اجرا کنید باید از اینترفیس استفاده کنید اما در این مقاله برای سادگی از این کار اجتناب می کنیم .

در شکل زیر شمایی که در آن ارتباط بین کنترلر و کلاس های context در دو حالت زمانی که repository وجود دارد و زمانی که وجود ندارد نمایش داده شده است .


در ادامه کلاس Student Repositoryرا ایجاد می کنیم
در داخل فولدر DAL یک کلاسی با نام IStudentRepository ایجاد می کنیم .

using System;
using System.Collections.Generic;
using ContosoUniversity.Models;

namespace ContosoUniversity.DAL
{
    public interface IStudentRepository : IDisposable
    {
        IEnumerable<Student> GetStudents();
        Student GetStudentByID(int studentId);
        void InsertStudent(Student student);
        void DeleteStudent(int studentID);
        void UpdateStudent(Student student);
        void Save();
    }
}


همان طور که می بینید در این کدها متدهایی برای پیاده سازی چهار عمل اصلی نوشته شده است البته این متدها پیاده سازی نشده اند برای اینکه آنها را داخل یک اینترفیس نوشته ایم و پیاده سازی آنها بر عهده کلاسی خواهد بود که از این اینترفیس ارث بری می کند .

حال در داخل همان پوشه DAL یک کلاس به نام StudentRepository را ایجاد می کنیم .این کلاس اینترفیس IStudentRepository را پیاده سازی خواهد کرد .

using System;
using System.Collections.Generic;
using System.Linq;
using System.Data;
using ContosoUniversity.Models;

namespace ContosoUniversity.DAL
{
    public class StudentRepository : IStudentRepository, IDisposable
    {
        private SchoolContext context;

        public StudentRepository(SchoolContext context)
        {
            this.context = context;
        }

        public IEnumerable<Student> GetStudents()
        {
            return context.Students.ToList();
        }

        public Student GetStudentByID(int id)
        {
            return context.Students.Find(id);
        }

        public void InsertStudent(Student student)
        {
            context.Students.Add(student);
        }

        public void DeleteStudent(int studentID)
        {
            Student student = context.Students.Find(studentID);
            context.Students.Remove(student);
        }

        public void UpdateStudent(Student student)
        {
            context.Entry(student).State = EntityState.Modified;
        }

        public void Save()
        {
            context.SaveChanges();
        }

        private bool disposed = false;

        protected virtual void Dispose(bool disposing)
        {
            if (!this.disposed)
            {
                if (disposing)
                {
                    context.Dispose();
                }
            }
            this.disposed = true;
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    }
}

کلاسی که قرار است نقش دیتابیس را برای ما بازی کند در اینجا به نام  schoolContext شناخته می شود .

  در داخل سازنده کلاس StudentRepository یک نمونه از این کلاس را ایجاد کرده ایم

private SchoolContext context;

public StudentRepository(SchoolContext context)
{
    this.context = context;
}

می توانستید از کلاس Context خود یک نمونه جدید ایجاد کنید اما اگر قصد داشتید در داخل یک کنترلر از چندین repository استفاده کنید از آنجایی که در داخل هر کدام از این Repository ها از Context خود نمونه ایجاد کرده ایم ، با این کار باعث می شدیم که به ازای هر repository به بانک اتصال برقرار کنیم وهر کدام از این repository ها هم به صورت مجزا با بانک کار می کردند و یک مفهوم واحد نداشتیم .

اما در داخل کنترلر course که از چندین repository  استفاده خواهیم کرد از الگوی طراحی Unit Of work استفاده می کنیم تا تمام کارها در یک Context انجام شود به این صورت فقط یک بار به بانک مراجعه کرده ایم و تمام اعمال با یک بار نمونه سازی از کلاس context انجام خواهد شد .

همانطور که دیدید کلاس های  repostitory اینترفیس IDisposable را نیز پیاده سازی می کنند .این اینترفیس به ما اطمینان می دهد که پس از اینکه درخواست انجام شده تمام اشیاء ساخته شده و از جمله کلاس context از حافظه پاک خواهند شد .

تغییر کنترلر Student  برای استفاده از Repository

کنترلر  student در زیر آمده است تغییراتی برای استفاده از Repository
 در آن به صورت روشن تر نشان داده شده است.
 

using System;
using System.Data;
using System.Linq;
using System.Web.Mvc;
using ContosoUniversity.Models;
using ContosoUniversity.DAL;
using PagedList;

namespace ContosoUniversity.Controllers
{
   public class StudentController : Controller
   {
      private IStudentRepository studentRepository;

      public StudentController()
      {
         this.studentRepository = new StudentRepository(new SchoolContext());
      }

      public StudentController(IStudentRepository studentRepository)
      {
         this.studentRepository = studentRepository;
      }

      //
      // GET: /Student/

      public ViewResult Index(string sortOrder, string currentFilter, string searchString, int? page)
      {
         ViewBag.CurrentSort = sortOrder;
         ViewBag.NameSortParm = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
         ViewBag.DateSortParm = sortOrder == "Date" ? "date_desc" : "Date";

         if (searchString != null)
         {
            page = 1;
         }
         else
         {
            searchString = currentFilter;
         }
         ViewBag.CurrentFilter = searchString;

         var students = from s in studentRepository.GetStudents()
                        select s;
         if (!String.IsNullOrEmpty(searchString))
         {
            students = students.Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())
                                   || s.FirstMidName.ToUpper().Contains(searchString.ToUpper()));
         }
         switch (sortOrder)
         {
            case "name_desc":
               students = students.OrderByDescending(s => s.LastName);
               break;
            case "Date":
               students = students.OrderBy(s => s.EnrollmentDate);
               break;
            case "date_desc":
               students = students.OrderByDescending(s => s.EnrollmentDate);
               break;
            default:  // Name ascending 
               students = students.OrderBy(s => s.LastName);
               break;
         }

         int pageSize = 3;
         int pageNumber = (page ?? 1);
         return View(students.ToPagedList(pageNumber, pageSize));
      }

      //
      // GET: /Student/Details/5

      public ViewResult Details(int id)
      {
         Student student = studentRepository.GetStudentByID(id);
         return View(student);
      }

      //
      // GET: /Student/Create

      public ActionResult Create()
      {
         return View();
      }

      //
      // POST: /Student/Create

      [HttpPost]
      [ValidateAntiForgeryToken]
      public ActionResult Create(
         [Bind(Include = "LastName, FirstMidName, EnrollmentDate")]
           Student student)
      {
         try
         {
            if (ModelState.IsValid)
            {
               studentRepository.InsertStudent(student);
               studentRepository.Save();
               return RedirectToAction("Index");
            }
         }
         catch (DataException /* dex */)
         {
            //Log the error (uncomment dex variable name after DataException and add a line here to write a log.
            ModelState.AddModelError(string.Empty, "Unable to save changes. Try again, and if the problem persists contact your system administrator.");
         }
         return View(student);
      }

      //
      // GET: /Student/Edit/5

      public ActionResult Edit(int id)
      {
         Student student = studentRepository.GetStudentByID(id);
         return View(student);
      }

      //
      // POST: /Student/Edit/5

      [HttpPost]
      [ValidateAntiForgeryToken]
      public ActionResult Edit(
         [Bind(Include = "LastName, FirstMidName, EnrollmentDate")]
         Student student)
      {
         try
         {
            if (ModelState.IsValid)
            {
               studentRepository.UpdateStudent(student);
               studentRepository.Save();
               return RedirectToAction("Index");
            }
         }
         catch (DataException /* dex */)
         {
            //Log the error (uncomment dex variable name after DataException and add a line here to write a log.
            ModelState.AddModelError(string.Empty, "Unable to save changes. Try again, and if the problem persists contact your system administrator.");
         }
         return View(student);
      }

      //
      // GET: /Student/Delete/5

      public ActionResult Delete(bool? saveChangesError = false, int id = 0)
      {
         if (saveChangesError.GetValueOrDefault())
         {
            ViewBag.ErrorMessage = "Delete failed. Try again, and if the problem persists see your system administrator.";
         }
         Student student = studentRepository.GetStudentByID(id);
         return View(student);
      }

      //
      // POST: /Student/Delete/5

      [HttpPost]
      [ValidateAntiForgeryToken]
      public ActionResult Delete(int id)
      {
         try
         {
            Student student = studentRepository.GetStudentByID(id);
            studentRepository.DeleteStudent(id);
            studentRepository.Save();
         }
         catch (DataException /* dex */)
         {
            //Log the error (uncomment dex variable name after DataException and add a line here to write a log.
            return RedirectToAction("Delete", new { id = id, saveChangesError = true });
         }
         return RedirectToAction("Index");
      }

      protected override void Dispose(bool disposing)
      {
         studentRepository.Dispose();
         base.Dispose(disposing);
      }
   }
}

همان طور که دیدید در داخل سازنده کنترلر یک شی از نوع  IStudentRepository در نظر گرفته ایم .این شی را با یک نمونه از کلاس StudentRepository مقدار دهی کرده ایم .خود کلاس StudentRepository،

کلاس SchoolContext  را در داخل سازنده خود به صورت نمونه سازی شده دارد .در ضمن در کدهای Linq و یا Entity Framework که نوشته ایم در واقع به studentRepository.GetStudents وصل شده ایم و دیگر به نمونه ای از کلاس Context یا  همان کلاس بانک خود مراجعه نکرده ایم .
تمام اعمالی که قرار است با داده های خود انجام دهیم از طریق کلاس repository انجام می دهیم .
به کدهای زیر دقت کنید و به این فکر کنید که دیگر متغیر db از نوع کلاس context نداریم و ثانیا اعمال saveChange() ، Add، find و غیره تماما از طریق کلاس repository انجام می شود .

var students = from s in studentRepository.GetStudents()
               select s;

Student student = studentRepository.GetStudentByID(id);

studentRepository.InsertStudent(student);
studentRepository.Save();

studentRepository.UpdateStudent(student);
studentRepository.Save();

studentRepository.DeleteStudent(id);
studentRepository.Save();


در ضمن دستور زیر حافظه را پاک می کند و اشیاء ایجاد شده از جمله شی Database Context را از بین می برد .

studentRepository.Dispose();


بار دیگر به متد ایندکس توجه کنید .یک تفاوت مهم در داخل این متد هست که کار فیلتر اطلاعات و مرتب سازی آنها را انجام می دهد .اگر از روش repository استفاده نکرده باشیم در داخل نمونه ای از شی context و داخل جدول Students اطلاعات را جمع آوری خواهیم کرد .مانند کد زیر

var students = from s in context.Students
               select s;
if (!String.IsNullOrEmpty(searchString))
{
    students = students.Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())
                           || s.FirstMidName.ToUpper().Contains(searchString.ToUpper()));
}


ولی اگر از الگوی repository استفاده کنیم کد ما به شکل زیر خواهد بود

 var students = from s in studentRepository.GetStudents()
                select s;
 if (!String.IsNullOrEmpty(searchString))
 {
     students = students.Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())
                            || s.FirstMidName.ToUpper().Contains(searchString.ToUpper()));
 }


تنها قسمتی که به رنگ زرد مشخص شده است تغییر کرده است .
اگر students از جنس IQueryable باشد کوئری ما نمی تواند به دیتابیس چیزی را ارسال کند مگر اینکه به کالکشن تبدیل شود.و این هم امکان پذیر نیست تا زمانی که View صفحه ایندکس به مدل student دسترسی داشته باشد .در ضمن همان طور که میدانید متد Where  تنها اطلاعاتی را به دیتابیس ارسال می کند که از فیلتر آن رد شده باشد در ضمن چون خروجی آن از نوع مجموعه یا کالکشن است نیازی به متد tolist ندارد .
با تغییر کردن context.Students به studentRepository.GetStudents() هر دو این کار ها باهم اتفاق خواهد افتاد .یعنی هم یک سری دیتای خاص که مد نظر ماست و هم به صورت مجموعه به سمت دیتابیس ارسال خواهد شد .
در این جالازم است کمی راجع به تفاوت IQueryable و IEnumerable هم صحبت کنیم .
وقتی متدی را از جنس IQueryable در نظر می گیرید هیچ چیزی به ما بر گردانده نمی شود ! منظور این است که هیج مجموعه ای برگشت داده نمی شود و تنها یک عبارت یا یک query از عبارت های مورد نظر شما تولید خواهد شد .یعنی رفت و برگشتی به جایی که اطلاعات را در آنها ذخیره کرده ایم اتفاق نمی افتد .زمانی که از متد tolist استفاده کنیم تازه در آن زمان اطلاعات ما به صورت یک مجموعه از دیتابیس واکشی می شود .البته با داشتن حلقه foreach هم اتفاق مشابه رخ می دهد و کوئری متناظر با کوئری IQueryable در سمت بانک اجرا می شود و حاصل بازکشت داده می شود .
خروجی IQueryable به معنای مشخص نبودن زمان اجرای نهایی کوئری و همچنین مبهم بودن نحوه‌ی استفاده از آن است. به همین جهت متدهایی را طراحی کنید که IEnumerable بازگشت می‌دهند اما در بدنه‌ی آن‌ها به نحو صحیح و مطلوبی از IQueryable استفاده شده است. به این صورت حد و مرز یک متد کاملا مشخص می‌شود.
به همان متد ایندکس در داخل کنترلر student توجه کنید در قسمت جستجو با نوشتن  کد زیر

var students = from s in studentRepository.GetStudents()

                           select s;

            if (!String.IsNullOrEmpty(searchString))

            {

                students = students.Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())

                                       || s.FirstMidName.ToUpper().Contains(searchString.ToUpper()));
            }

 

اطلاعات مورد نیاز خود را از بانک واکشی کرده ایم ، کوئری مشابه ایی که در سمت بانک اجرا خواهد شد مانند زیراست

 

SELECT 
'0X0X' AS [C1], 
[Extent1].[PersonID] AS [PersonID], 
[Extent1].[LastName] AS [LastName], 
[Extent1].[FirstName] AS [FirstName], 
[Extent1].[EnrollmentDate] AS [EnrollmentDate]
FROM [dbo].[Person] AS [Extent1]
WHERE [Extent1].[Discriminator] = N'Student'


این کد اطلاعات تمام دانشجویان را به ما می دهد و هیچ اطلاعی از عبارت مورد جستجو که فیلتر ماست ندارد .تمام کارهای جستجو ، صفحه بندی ، فیلتر کردن داده ها تنها بعد از فراخوانی مجموعه IEnumerable اتفاق می افتد .
تا این زمان رفت و برگشتی به بانک اطلاعاتی صورت نگرفته است .وقتی متد ToPageList فراخوانی شد اطلاعات از بانک واکشی خواهد شد .

زمانی که شی IQueryable متد ToPagedList را فراخوانی کرد تازه در آن زمان رشته مورد جستجو به بانک ارسال می شود و اطلاعات جداول ما را فیلتر کرده و نتیجه را بر می گرداند .
کوئری مشابه ایی که اجرا خواهد شد مانند زیر است

exec sp_executesql N'SELECT TOP (3) 
[Project1].[StudentID] AS [StudentID], 
[Project1].[LastName] AS [LastName], 
[Project1].[FirstName] AS [FirstName], 
[Project1].[EnrollmentDate] AS [EnrollmentDate]
FROM ( SELECT [Project1].[StudentID] AS [StudentID], [Project1].[LastName] AS [LastName], [Project1].[FirstName] AS [FirstName], [Project1].[EnrollmentDate] AS [EnrollmentDate], row_number() OVER (ORDER BY [Project1].[LastName] ASC) AS [row_number]
	FROM ( SELECT 
		[Extent1].[StudentID] AS [StudentID], 
		[Extent1].[LastName] AS [LastName], 
		[Extent1].[FirstName] AS [FirstName], 
		[Extent1].[EnrollmentDate] AS [EnrollmentDate]
		FROM [dbo].[Student] AS [Extent1]
		WHERE (( CAST(CHARINDEX(UPPER(@p__linq__0), UPPER([Extent1].[LastName])) AS int)) > 0) OR (( CAST(CHARINDEX(UPPER(@p__linq__1), UPPER([Extent1].[FirstName])) AS int)) > 0)
	)  AS [Project1]
)  AS [Project1]
WHERE [Project1].[row_number] > 0
ORDER BY [Project1].[LastName] ASC',N'@p__linq__0 nvarchar(4000),@p__linq__1 nvarchar(4000)',@p__linq__0=N'Alex',@p__linq__1=N'Alex'


پیاده سازی کلاس Generic Repository و کلاس Unit of Work
مسلما پیاده سازی کلاس repository مختص هر موجودیت باعث افزونگی در کدهای ما خواهد شد و مدام در طول برنامه یک سری کدها را تکرار می کنیم .
برای نمونه تصور کنید که قرار است دو موجودیت مختلف که در یک تراکنش قرار دارند را به روزرسانی کنید ، اگر هر کدام از این موجودیت ها به صورت جداگانه ای از کلاس Context استفاده کنند ممکن است یک به روزرسانی موفق باشد و دیگری نباشد .
یک روش برای جلوگیری از افزونگی کدها استفاده از کلاس Generic Repository است .و یک راه برای اینکه تمام این repository ها از یک نمونه کلاس context و یا همان کلاس دیتابیس  استفاده کنند این است که از کلاس unit of work استفاده کنیم .
در ادامه کلاس جنریک repository و کلاس Unit Of work را ایجاد خواهیم کرد .از این کلاس ها در داخل کنترلر course استفاده خواهیم کرد .چون در داخل این کنترلر به هر دو موجودیت های Department و بایدCourse دسترسی داشته باشیم.برای سادگی برای کلاس های جنریک repository و کلاس unit of work اینترفیس ایجاد نمی کنیم .
ساخت کلاس Generic Repository
در داخل پوشه DAL یک کلاس به نام GenericRepository ایجاد کنید .در داخل این کلاس کدهای زیر را بنویسید
 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Data;
using System.Data.Entity;
using ContosoUniversity.Models;
using System.Linq.Expressions;

namespace ContosoUniversity.DAL
{
    public class GenericRepository<TEntity> where TEntity : class
    {
        internal SchoolContext context;
        internal DbSet<TEntity> dbSet;

        public GenericRepository(SchoolContext context)
        {
            this.context = context;
            this.dbSet = context.Set<TEntity>();
        }

        public virtual IEnumerable<TEntity> Get(
            Expression<Func<TEntity, bool>> filter = null,
            Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
            string includeProperties = "")
        {
            IQueryable<TEntity> query = dbSet;

            if (filter != null)
            {
                query = query.Where(filter);
            }

            foreach (var includeProperty in includeProperties.Split
                (new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
            {
                query = query.Include(includeProperty);
            }

            if (orderBy != null)
            {
                return orderBy(query).ToList();
            }
            else
            {
                return query.ToList();
            }
        }

        public virtual TEntity GetByID(object id)
        {
            return dbSet.Find(id);
        }

        public virtual void Insert(TEntity entity)
        {
            dbSet.Add(entity);
        }

        public virtual void Delete(object id)
        {
            TEntity entityToDelete = dbSet.Find(id);
            Delete(entityToDelete);
        }

        public virtual void Delete(TEntity entityToDelete)
        {
            if (context.Entry(entityToDelete).State == EntityState.Detached)
            {
                dbSet.Attach(entityToDelete);
            }
            dbSet.Remove(entityToDelete);
        }

        public virtual void Update(TEntity entityToUpdate)
        {
            dbSet.Attach(entityToUpdate);
            context.Entry(entityToUpdate).State = EntityState.Modified;
        }
    }
}

در داخل این کلاس دو متغیر Internal از نوع های SchoolContext  و dbset تعریف می کنیم .سازنده این کلاس یک پارامتر از نوع SchoolContext  می پذیرد .این پارامتر در داخل همان متغیر Internal به نام context ریخته می شود .همچنین در داخل متغیر Dbset هم یک موجودیت از کلاس context ریخته می شود


internal SchoolContext context;
internal DbSet dbSet;

سازنده به شکل زیر خواهد بود

public GenericRepository(SchoolContext context)
{
    this.context = context;
    this.dbSet = context.Set<TEntity>();
}

متد Get در داخل کلاس repository جنریک از عبارات لامبدا استفاده کرده است .با این کار وقتی این متد را فراخوانی می کنیم می توانیم فیلتر دلخواه خود و همین طور ستونی که می خواهیم به عنوان نتیجه نمایش دهیم

public virtual IEnumerable<TEntity> Get(
    Expression<Func<TEntity, bool>> filter = null,
    Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
    string includeProperties = "")

تکه کد  Expression<Func<TEntity, bool>> filterبه این معنی است که فراخوان (کسی که تابع را فراخوانی می کند)باید یک لامبدا بر اساس نوع TEntity را برای تابع فراخوانی کند .و این قسمت یک خروجی از نوع bool بر می گرداند .

اگر به عنوان مثال کلاس جنریک repository برای موجودیت Student نمونه سازی شود تکه کد فیلتر ما به صورت student => student.LastName == "Smith" خواهد بود .البته این یک مثال است و این کد به حالت های مختلف می تواند داده های ما را فیلتر کند .

در همان تابع Get که در داخل کلاس جنریک repository نوشتیم و در داخل پارامتر دوم آن تکه کد Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy نوشته شده است .این تکه کد مشخص می کند که کسی که قرار است تابع Get را فراخوانی کند باید برای پارامتر دوم آن یک عبارت لامبدا آماده کند و به آن ارسال کند .ولی در این مورد باید شی ما از نوع IQueryable و بر اساس موجودیت TEntity باشد .این عبارت لامبدا یک لیست مرتب شده از شی IQueryable بر می گرداند .اگر کلاس repository برای موجودیت student نمونه سازی شود قسمت دوم پارامتر چیزی شبیه کد q => q.OrderBy(s => s.LastName) خواهد بود .

تابع Get یک شی IQueryable ایجاد می کند و سپس فیلتر را به این شی اعمال می کند

IQueryable<TEntity> query = dbSet;

if (filter != null)
{
    query = query.Where(filter);
}

در مرحله بعد eager-loading بر روی خروجی اعمال خواهد شد .و کاما ها حذف خواهند شد

foreach (var includeProperty in includeProperties.Split
    (new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) 
{ 
    query = query.Include(includeProperty); 
}

نهایتا پارامتر دوم تابع Get یا همان orderby آماده است !

if (orderBy != null)
{
    return orderBy(query).ToList();
}
else
{
    return query.ToList();
}

زمانی که تابع Get را فراخوانی می کنید هر دو عمل مرتب سازی و فیلترینگ را بر روی داده ها اعمال می کنید .

بعد از این تابع توابع insert و GetByID بسیار شبیه همان حالت repository سابق بدون حالت جنریک هستند .فقط توجه داشته باشید که در تابع insert نمی توانید eager Loading داشته باشید چون اساسا قادر نیستید که eager loading را برای متد Find از توابع EF داشته باشید .

برای تابع Delete دو عدد overload نوشته ایم

public virtual void Delete(object id)
{
    TEntity entityToDelete = dbSet.Find(id);
    dbSet.Remove(entityToDelete);
}

public virtual void Delete(TEntity entityToDelete)
{
            if (context.Entry(entityToDelete).State == EntityState.Detached)
            {
                dbSet.Attach(entityToDelete);
            }
            dbSet.Remove(entityToDelete);

}

یکی از این متدها فقط یک پارامتر id می گیرد و بر اساس آن Id موجودیت را یافته و حذف می کند .و سربار دوم این تابع به جای id یک موجودیت کامل می گیرد.

ساخت کلاس Unit Of Work

کلاس Unit Of Work یک هدف دارد و آن هم این است که این اطمینان در داخل برنامه وجود داشته باشد که زمانی که از چندین repository در داخل برنامه استفاده می کنید تمام این repository ها از یک context یا همان کلاس نماینده دیتابیس استفاده می کنند .

در داخل پوشه DAL یک کلاسی به نام UnitOfWork.cs ایجاد کنید .کدهای این کلاس در زیر نشان داده شده است

using System;
using ContosoUniversity.Models;

namespace ContosoUniversity.DAL
{
    public class UnitOfWork : IDisposable
    {
        private SchoolContext context = new SchoolContext();
        private GenericRepository<Department> departmentRepository;
        private GenericRepository<Course> courseRepository;

        public GenericRepository<Department> DepartmentRepository
        {
            get
            {

                if (this.departmentRepository == null)
                {
                    this.departmentRepository = new GenericRepository<Department>(context);
                }
                return departmentRepository;
            }
        }

        public GenericRepository<Course> CourseRepository
        {
            get
            {

                if (this.courseRepository == null)
                {
                    this.courseRepository = new GenericRepository<Course>(context);
                }
                return courseRepository;
            }
        }

        public void Save()
        {
            context.SaveChanges();
        }

        private bool disposed = false;

        protected virtual void Dispose(bool disposing)
        {
            if (!this.disposed)
            {
                if (disposing)
                {
                    context.Dispose();
                }
            }
            this.disposed = true;
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    }
}

در داخل این کلاس فیلد هایی از نوع کلاس دیتابیس (SchoolContext)و متغیرهایی از نوع کلاس جنریک repository از نوع هر کدام از موجودیت هایمان وجود دارد .

private SchoolContext context = new SchoolContext();
private GenericRepository<Department> departmentRepository;
private GenericRepository<Course> courseRepository;

در داخل این کلاس چک می کنیم که آیا repository  ما از قبل وجود دارد یا نه ؟ به این ترتیب همیشه یک نوع کلاس repository جنریک از نوع یک موجودیت خاص خواهیم داشت .

public GenericRepository<Department> DepartmentRepository
{
    get
    {

        if (this.departmentRepository == null)
        {
            this.departmentRepository = new GenericRepository<Department>(context);
        }
        return departmentRepository;
    }
}

در داخل متد Save تابع SaveChanges را فراخوانی کرده ایم .

مانند هر کلاس دیگری که اگر از context نمونه بسازد آنرا در نهایت از حافظه پاک می کند این کلاس هم از اینترفیس IDisposable ارث بری دارد و همچنین تابع dispose را هم پیاده سازی کرده است .

تغییر کنترلر Course  برای استفاد ه از کلاس UnitOfWork  و کلاس های Repository

اساسا همان طور که گفته شد چون در داخل کنترلر course ناچار بودیم از هر دو repository مربوط به دو موجودیت دیگر هم استفاده کنیم و در داخل هر کدام از اینها از کلاس context نمونه گرفته می شد و کار به روزرسانی ما مختل می شدو ...... ناچار شدیم از الگوی طراحی unit of work استفاده کنیم .

کدهای این کنترلر در زیر نشان داده شده است

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using ContosoUniversity.Models;
using ContosoUniversity.DAL;

namespace ContosoUniversity.Controllers
{
   public class CourseController : Controller
   {
      private UnitOfWork unitOfWork = new UnitOfWork();

      //
      // GET: /Course/

      public ViewResult Index()
      {
         var courses = unitOfWork.CourseRepository.Get(includeProperties: "Department");
         return View(courses.ToList());
      }

      //
      // GET: /Course/Details/5

      public ViewResult Details(int id)
      {
         Course course = unitOfWork.CourseRepository.GetByID(id);
         return View(course);
      }

      //
      // GET: /Course/Create

      public ActionResult Create()
      {
         PopulateDepartmentsDropDownList();
         return View();
      }

      [HttpPost]
      [ValidateAntiForgeryToken]
      public ActionResult Create(
          [Bind(Include = "CourseID,Title,Credits,DepartmentID")]
         Course course)
      {
         try
         {
            if (ModelState.IsValid)
            {
               unitOfWork.CourseRepository.Insert(course);
               unitOfWork.Save();
               return RedirectToAction("Index");
            }
         }
         catch (DataException /* dex */)
         {
            //Log the error (uncomment dex variable name after DataException and add a line here to write a log.)
            ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
         }
         PopulateDepartmentsDropDownList(course.DepartmentID);
         return View(course);
      }

      public ActionResult Edit(int id)
      {
         Course course = unitOfWork.CourseRepository.GetByID(id);
         PopulateDepartmentsDropDownList(course.DepartmentID);
         return View(course);
      }

      [HttpPost]
      [ValidateAntiForgeryToken]
      public ActionResult Edit(
           [Bind(Include = "CourseID,Title,Credits,DepartmentID")]
         Course course)
      {
         try
         {
            if (ModelState.IsValid)
            {
               unitOfWork.CourseRepository.Update(course);
               unitOfWork.Save();
               return RedirectToAction("Index");
            }
         }
         catch (DataException /* dex */)
         {
            //Log the error (uncomment dex variable name after DataException and add a line here to write a log.)
            ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
         }
         PopulateDepartmentsDropDownList(course.DepartmentID);
         return View(course);
      }

      private void PopulateDepartmentsDropDownList(object selectedDepartment = null)
      {
         var departmentsQuery = unitOfWork.DepartmentRepository.Get(
             orderBy: q => q.OrderBy(d => d.Name));
         ViewBag.DepartmentID = new SelectList(departmentsQuery, "DepartmentID", "Name", selectedDepartment);
      }

      //
      // GET: /Course/Delete/5

      public ActionResult Delete(int id)
      {
         Course course = unitOfWork.CourseRepository.GetByID(id);
         return View(course);
      }

      //
      // POST: /Course/Delete/5

      [HttpPost, ActionName("Delete")]
      [ValidateAntiForgeryToken]
      public ActionResult DeleteConfirmed(int id)
      {
         Course course = unitOfWork.CourseRepository.GetByID(id);
         unitOfWork.CourseRepository.Delete(id);
         unitOfWork.Save();
         return RedirectToAction("Index");
      }

      protected override void Dispose(bool disposing)
      {
         unitOfWork.Dispose();
         base.Dispose(disposing);
      }
   }
}

همان طور که می بینید در این کنترلر از کلاس unit of work نمونه سازی شده است .

private UnitOfWork unitOfWork = new UnitOfWork();

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

var courses = unitOfWork.CourseRepository.Get(includeProperties: "Department");
// ...
Course course = unitOfWork.CourseRepository.GetByID(id);
// ...
unitOfWork.CourseRepository.Insert(course);
unitOfWork.Save();
// ...
Course course = unitOfWork.CourseRepository.GetByID(id);
// ...
unitOfWork.CourseRepository.Update(course);
unitOfWork.Save();
// ...
var departmentsQuery = unitOfWork.DepartmentRepository.Get(
    orderBy: q => q.OrderBy(d => d.Name));
// ...
Course course = unitOfWork.CourseRepository.GetByID(id);
// ...
unitOfWork.CourseRepository.Delete(id);
unitOfWork.Save();
// ...
unitOfWork.Dispose();

متد dispose هم نمونه ایجاد شده از کلاس UnitOfWork را از حافظه پاک می کند

حال برنامه را اجرا کنید و وارد تب Course شوید .

تا به اینجای کار هم الگوی unit of work و هم الگوی repository را پیاده سازی کرده ایم .در ضمن در داخل کلاس repository از توابع لامبدا استفاده کردیم .

ایمان مدائنی

نویسنده 1299 مقاله در برنامه نویسان

کاربرانی که از نویسنده این مقاله تشکر کرده اند

در صورتی که در رابطه با این مقاله سوالی دارید، در تاپیک های انجمن مطرح کنید