Menerapkan Repositori dan Unit Pola Kerja dalam Aplikasi MVC ASP.NET (9 dari 10)

oleh Tom Dykstra

Aplikasi web sampel Contoso University menunjukkan cara membuat aplikasi ASP.NET MVC 4 menggunakan Entity Framework 5 Code First dan Visual Studio 2012. Untuk informasi tentang seri tutorial, lihat tutorial pertama dalam seri ini.

Catatan

Jika Anda mengalami masalah yang tidak dapat Anda atasi, unduh bab yang telah selesai dan coba reprodurasi masalah Anda. Anda umumnya dapat menemukan solusi untuk masalah dengan membandingkan kode Anda dengan kode yang telah selesai. Untuk beberapa kesalahan umum dan cara mengatasinya, lihat Kesalahan dan Solusi.

Dalam tutorial sebelumnya Anda menggunakan pewarisan untuk mengurangi kode redundan di Student kelas entitas dan Instructor . Dalam tutorial ini Anda akan melihat beberapa cara untuk menggunakan repositori dan unit pola kerja untuk operasi CRUD. Seperti dalam tutorial sebelumnya, dalam tutorial ini Anda akan mengubah cara kode Anda bekerja dengan halaman yang sudah Anda buat daripada membuat halaman baru.

Repositori dan Unit Pola Kerja

Repositori dan unit pola kerja dimaksudkan untuk membuat lapisan abstraksi antara lapisan akses data dan lapisan logika bisnis aplikasi. Menerapkan pola ini dapat membantu mengisolasi aplikasi Anda dari perubahan di penyimpanan data dan dapat memfasilitasi pengujian unit otomatis atau pengembangan berbasis pengujian (TDD).

Dalam tutorial ini Anda akan menerapkan kelas repositori untuk setiap jenis entitas. Student Untuk jenis entitas, Anda akan membuat antarmuka repositori dan kelas repositori. Saat membuat instans repositori di pengontrol, Anda akan menggunakan antarmuka sehingga pengontrol akan menerima referensi ke objek apa pun yang mengimplementasikan antarmuka repositori. Ketika pengontrol berjalan di bawah server web, pengontrol menerima repositori yang berfungsi dengan Kerangka Kerja Entitas. Ketika pengontrol berjalan di bawah kelas pengujian unit, pengontrol menerima repositori yang berfungsi dengan data yang disimpan dengan cara yang dapat Anda manipulasi dengan mudah untuk pengujian, seperti koleksi dalam memori.

Nantinya dalam tutorial, Anda akan menggunakan beberapa repositori dan unit kelas kerja untuk Course jenis entitas dan Department di Course pengontrol. Unit kelas kerja mengoordinasikan pekerjaan beberapa repositori dengan membuat satu kelas konteks database yang dibagikan oleh semuanya. Jika Anda ingin dapat melakukan pengujian unit otomatis, Anda akan membuat dan menggunakan antarmuka untuk kelas-kelas ini dengan cara yang sama seperti yang Anda lakukan untuk Student repositori. Namun, untuk menjaga tutorial tetap sederhana, Anda akan membuat dan menggunakan kelas-kelas ini tanpa antarmuka.

Ilustrasi berikut menunjukkan salah satu cara untuk mengonsepkan hubungan antara pengontrol dan kelas konteks dibandingkan dengan tidak menggunakan repositori atau unit pola kerja sama sekali.

Repository_pattern_diagram

Anda tidak akan membuat pengujian unit dalam seri tutorial ini. Untuk pengenalan TDD dengan aplikasi MVC yang menggunakan pola repositori, lihat Panduan: Menggunakan TDD dengan ASP.NET MVC. Untuk informasi selengkapnya tentang pola repositori, lihat sumber daya berikut ini:

Catatan

Ada banyak cara untuk mengimplementasikan repositori dan unit pola kerja. Anda dapat menggunakan kelas repositori dengan atau tanpa unit kelas kerja. Anda dapat menerapkan satu repositori untuk semua jenis entitas, atau satu untuk setiap jenis. Jika Anda menerapkannya untuk setiap jenis, Anda dapat menggunakan kelas terpisah, kelas dasar generik dan kelas turunan, atau kelas dasar abstrak dan kelas turunan. Anda dapat menyertakan logika bisnis di repositori Anda atau membatasinya ke logika akses data. Anda juga dapat membangun lapisan abstraksi ke dalam kelas konteks database Anda dengan menggunakan antarmuka IDbSet di sana alih-alih jenis DbSet untuk set entitas Anda. Pendekatan untuk menerapkan lapisan abstraksi yang ditunjukkan dalam tutorial ini adalah salah satu opsi untuk Anda pertimbangkan, bukan rekomendasi untuk semua skenario dan lingkungan.

Membuat Kelas Repositori Siswa

Di folder DAL , buat file kelas bernama IStudentRepository.cs dan ganti kode yang ada dengan kode berikut:

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();
    }
}

Kode ini mendeklarasikan serangkaian metode CRUD yang khas, termasuk dua metode baca - satu yang mengembalikan semua Student entitas, dan satu yang menemukan satu Student entitas berdasarkan ID.

Di folder DAL , buat file kelas bernama file StudentRepository.cs . Ganti kode yang ada dengan kode berikut, yang mengimplementasikan IStudentRepository antarmuka:

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);
        }
    }
}

Konteks database didefinisikan dalam variabel kelas, dan konstruktor mengharapkan objek panggilan untuk meneruskan instans konteks:

private SchoolContext context;

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

Anda dapat membuat instans konteks baru di repositori, tetapi kemudian jika Anda menggunakan beberapa repositori dalam satu pengontrol, masing-masing akan berakhir dengan konteks terpisah. Nantinya Anda akan menggunakan beberapa repositori di Course pengontrol, dan Anda akan melihat bagaimana unit kelas kerja dapat memastikan bahwa semua repositori menggunakan konteks yang sama.

Repositori mengimplementasikan IDisposable dan membuang konteks database seperti yang Anda lihat sebelumnya di pengontrol, dan metode CRUD-nya melakukan panggilan ke konteks database dengan cara yang sama seperti yang Anda lihat sebelumnya.

Mengubah Pengontrol Siswa untuk Menggunakan Repositori

Di StudentController.cs, ganti kode yang saat ini ada di kelas dengan kode berikut. Perubahan disorot.

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);
      }
   }
}

Pengontrol sekarang mendeklarasikan variabel kelas untuk objek yang mengimplementasikan IStudentRepository antarmuka alih-alih kelas konteks:

private IStudentRepository studentRepository;

Konstruktor default (tanpa parameter) membuat instans konteks baru, dan konstruktor opsional memungkinkan pemanggil untuk meneruskan instans konteks.

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

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

(Jika Anda menggunakan injeksi dependensi, atau DI, Anda tidak memerlukan konstruktor default karena perangkat lunak DI akan memastikan bahwa objek repositori yang benar akan selalu disediakan.)

Dalam metode CRUD, repositori sekarang dipanggil alih-alih konteks:

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();

Dispose Dan metode sekarang membuang repositori alih-alih konteks:

studentRepository.Dispose();

Jalankan situs dan klik tab Siswa .

Students_Index_page

Halaman terlihat dan berfungsi sama seperti sebelum Anda mengubah kode untuk menggunakan repositori, dan halaman Siswa lainnya juga berfungsi sama. Namun, ada perbedaan penting dalam cara Index metode pengontrol melakukan pemfilteran dan pemesanan. Versi asli metode ini berisi kode berikut:

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()));
}

Metode yang diperbarui Index berisi kode berikut:

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()));
}

Hanya kode yang disorot yang telah berubah.

Dalam versi asli kode, students ditik sebagai IQueryable objek. Kueri tidak dikirim ke database sampai dikonversi menjadi koleksi menggunakan metode seperti ToList, yang tidak terjadi sampai tampilan Indeks mengakses model siswa. Metode Where dalam kode asli di atas menjadi klausa WHERE dalam kueri SQL yang dikirim ke database. Itu pada gilirannya berarti bahwa hanya entitas yang dipilih yang dikembalikan oleh database. Namun, sebagai hasil dari mengubah context.Students ke studentRepository.GetStudents(), students variabel setelah pernyataan ini adalah IEnumerable koleksi yang mencakup semua siswa dalam database. Hasil akhir penerapan Where metode ini sama, tetapi sekarang pekerjaan dilakukan dalam memori di server web dan bukan oleh database. Untuk kueri yang mengembalikan data dalam volume besar, ini bisa menjadi tidak efisien.

Tip

IQueryable vs. IEnumerable

Setelah Anda menerapkan repositori seperti yang diperlihatkan di sini, bahkan jika Anda memasukkan sesuatu di kotak Pencarian, kueri yang dikirim ke SQL Server mengembalikan semua baris Siswa karena tidak menyertakan kriteria pencarian Anda:

Cuplikan layar kode yang menunjukkan repositori siswa baru yang diimplementasikan dan disorot.

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'

Kueri ini mengembalikan semua data siswa karena repositori menjalankan kueri tanpa mengetahui tentang kriteria pencarian. Proses pengurutan, penerapan kriteria pencarian, dan memilih subset data untuk penomoran (hanya memperlihatkan 3 baris dalam hal ini) dilakukan dalam memori nanti ketika ToPagedList metode dipanggil pada IEnumerable koleksi.

Dalam versi kode sebelumnya (sebelum Anda menerapkan repositori), kueri tidak dikirim ke database sampai setelah Anda menerapkan kriteria pencarian, ketika ToPagedList dipanggil pada IQueryable objek.

Cuplikan layar yang memperlihatkan kode Pengontrol Siswa. Baris string pencarian kode dan baris Daftar Ke Halaman kode disorot.

Ketika ToPagedList dipanggil pada IQueryable objek, kueri yang dikirim ke SQL Server menentukan string pencarian, dan sebagai hasilnya hanya baris yang memenuhi kriteria pencarian yang dikembalikan, dan tidak ada pemfilteran yang perlu dilakukan dalam memori.

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'

(Tutorial berikut menjelaskan cara memeriksa kueri yang dikirim ke SQL Server.)

Bagian berikut ini memperlihatkan cara menerapkan metode repositori yang memungkinkan Anda menentukan bahwa pekerjaan ini harus dilakukan oleh database.

Anda sekarang telah membuat lapisan abstraksi antara pengontrol dan konteks database Kerangka Kerja Entitas. Jika Anda akan melakukan pengujian unit otomatis dengan aplikasi ini, Anda dapat membuat kelas repositori alternatif dalam proyek pengujian unit yang mengimplementasikan IStudentRepository. Alih-alih memanggil konteks untuk membaca dan menulis data, kelas repositori tiruan ini dapat memanipulasi koleksi dalam memori untuk menguji fungsi pengontrol.

Menerapkan Repositori Generik dan Unit Kelas Kerja

Membuat kelas repositori untuk setiap jenis entitas dapat menghasilkan banyak kode redundan, dan dapat mengakibatkan pembaruan parsial. Misalnya, Anda harus memperbarui dua jenis entitas yang berbeda sebagai bagian dari transaksi yang sama. Jika masing-masing menggunakan instans konteks database terpisah, satu mungkin berhasil dan yang lain mungkin gagal. Salah satu cara untuk meminimalkan kode redundan adalah dengan menggunakan repositori generik, dan salah satu cara untuk memastikan bahwa semua repositori menggunakan konteks database yang sama (dan dengan demikian mengoordinasikan semua pembaruan) adalah dengan menggunakan unit kelas kerja.

Di bagian tutorial ini, Anda akan membuat GenericRepository kelas dan UnitOfWork kelas, dan menggunakannya di Course pengontrol untuk mengakses Department set entitas dan Course . Seperti yang dijelaskan sebelumnya, untuk menjaga bagian tutorial ini tetap sederhana, Anda tidak membuat antarmuka untuk kelas-kelas ini. Tetapi jika Anda akan menggunakannya untuk memfasilitasi TDD, Anda biasanya akan mengimplementasikannya dengan antarmuka dengan cara yang sama seperti Anda melakukan Student repositori.

Membuat Repositori Generik

Di folder DAL , buat GenericRepository.cs dan ganti kode yang ada dengan kode berikut:

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;
        }
    }
}

Variabel kelas dideklarasikan untuk konteks database dan untuk kumpulan entitas yang digunakan untuk repositori:

internal SchoolContext context;
internal DbSet dbSet;

Konstruktor menerima instans konteks database dan menginisialisasi variabel kumpulan entitas:

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

Metode ini Get menggunakan ekspresi lambda untuk memungkinkan kode panggilan menentukan kondisi filter dan kolom untuk mengurutkan hasil, dan parameter string memungkinkan pemanggil menyediakan daftar properti navigasi yang dibatasi koma untuk pemuatan yang ingin dimuat:

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

Kode Expression<Func<TEntity, bool>> filter berarti pemanggil akan memberikan ekspresi lambda berdasarkan TEntity jenisnya, dan ekspresi ini akan mengembalikan nilai Boolean. Misalnya, jika repositori dibuat untuk Student jenis entitas, kode dalam metode panggilan mungkin menentukan student => student.LastName == "Smith" untuk filter parameter .

Kode Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy juga berarti pemanggil akan memberikan ekspresi lambda. Tetapi dalam hal ini, input ke ekspresi adalah IQueryable objek untuk jenis tersebut TEntity . Ekspresi akan mengembalikan versi objek yang IQueryable diurutkan. Misalnya, jika repositori dibuat untuk Student jenis entitas, kode dalam metode panggilan mungkin menentukan q => q.OrderBy(s => s.LastName) untuk orderBy parameter .

Kode dalam Get metode membuat IQueryable objek lalu menerapkan ekspresi filter jika ada:

IQueryable<TEntity> query = dbSet;

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

Selanjutnya menerapkan ekspresi pemuatan bersemangat setelah mengurai daftar yang dibatasi koma:

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

Terakhir, ini menerapkan orderBy ekspresi jika ada dan mengembalikan hasilnya; jika tidak, ekspresi mengembalikan hasil dari kueri yang tidak berurut:

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

Ketika Anda memanggil Get metode , Anda dapat melakukan pemfilteran dan pengurutan pada IEnumerable koleksi yang dikembalikan oleh metode alih-alih memberikan parameter untuk fungsi-fungsi ini. Tetapi pekerjaan pengurutan dan pemfilteran kemudian akan dilakukan dalam memori di server web. Dengan menggunakan parameter ini, Anda memastikan bahwa pekerjaan dilakukan oleh database daripada server web. Alternatifnya adalah membuat kelas turunan untuk jenis entitas tertentu dan menambahkan metode khusus Get , seperti GetStudentsInNameOrder atau GetStudentsByName. Namun, dalam aplikasi yang kompleks, ini dapat menghasilkan sejumlah besar kelas turunan dan metode khusus, yang bisa lebih banyak bekerja untuk dipertahankan.

Kode dalam GetByIDmetode , Insert, dan Update mirip dengan apa yang Anda lihat di repositori non-generik. (Anda tidak menyediakan parameter pemuatan yang bersemangat dalam GetByID tanda tangan, karena Anda tidak dapat melakukan pemuatan yang ingin dimuat dengan metode . Find )

Dua kelebihan beban disediakan untuk metode :Delete

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);
}

Salah satunya memungkinkan Anda meneruskan hanya ID entitas yang akan dihapus, dan satu mengambil instans entitas. Seperti yang Anda lihat dalam tutorial Menangani Konkurensi , untuk penanganan konkurensi, Anda memerlukan Delete metode yang mengambil instans entitas yang menyertakan nilai asli properti pelacakan.

Repositori generik ini akan menangani persyaratan CRUD yang khas. Ketika jenis entitas tertentu memiliki persyaratan khusus, seperti pemfilteran atau pemesanan yang lebih kompleks, Anda dapat membuat kelas turunan yang memiliki metode tambahan untuk jenis tersebut.

Membuat Unit Kelas Kerja

Unit kelas kerja melayani satu tujuan: untuk memastikan bahwa ketika Anda menggunakan beberapa repositori, mereka berbagi satu konteks database. Dengan begitu, ketika unit pekerjaan selesai, Anda dapat memanggil SaveChanges metode pada instans konteks tersebut dan yakin bahwa semua perubahan terkait akan dikoordinasikan. Semua yang dibutuhkan kelas adalah Save metode dan properti untuk setiap repositori. Setiap properti repositori mengembalikan instans repositori yang telah dibuat menggunakan instans konteks database yang sama dengan instans repositori lainnya.

Di folder DAL , buat file kelas bernama UnitOfWork.cs dan ganti kode templat dengan kode berikut:

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);
        }
    }
}

Kode membuat variabel kelas untuk konteks database dan setiap repositori. context Untuk variabel , konteks baru dibuat:

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

Setiap properti repositori memeriksa apakah repositori sudah ada. Jika tidak, itu membuat instans repositori, meneruskan dalam instans konteks. Akibatnya, semua repositori berbagi instans konteks yang sama.

public GenericRepository<Department> DepartmentRepository
{
    get
    {

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

Metode ini Save memanggil SaveChanges konteks database.

Seperti kelas apa pun yang membuat instans konteks database dalam variabel kelas, UnitOfWork kelas mengimplementasikan IDisposable dan membuang konteks.

Mengubah Pengontrol Kursus untuk menggunakan Kelas dan Repositori UnitOfWork

Ganti kode yang saat ini Anda miliki di CourseController.cs dengan kode berikut:

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);
      }
   }
}

Kode ini menambahkan variabel kelas untuk UnitOfWork kelas . (Jika Anda menggunakan antarmuka di sini, Anda tidak akan menginisialisasi variabel di sini; sebagai gantinya, Anda akan menerapkan pola dua konstruktor seperti yang Anda lakukan untuk Student repositori.)

private UnitOfWork unitOfWork = new UnitOfWork();

Di kelas lainnya, semua referensi ke konteks database digantikan oleh referensi ke repositori yang sesuai, menggunakan UnitOfWork properti untuk mengakses repositori. Metode Dispose membuang instans 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();

Jalankan situs dan klik tab Kursus .

Courses_Index_page

Halaman terlihat dan berfungsi sama seperti sebelum perubahan Anda, dan halaman Kursus lainnya juga berfungsi sama.

Ringkasan

Anda sekarang telah menerapkan repositori dan unit pola kerja. Anda telah menggunakan ekspresi lambda sebagai parameter metode di repositori generik. Untuk informasi selengkapnya tentang cara menggunakan ekspresi ini dengan IQueryable objek, lihat Antarmuka IQueryable(T) (System.Linq) di Pustaka MSDN. Dalam tutorial berikutnya, Anda akan mempelajari cara menangani beberapa skenario tingkat lanjut.

Tautan ke sumber daya Kerangka Kerja Entitas lainnya dapat ditemukan di ASP.NET Peta Konten Akses Data.