Udostępnij za pośrednictwem


Implementowanie repozytorium i jednostki wzorców pracy w aplikacji MVC ASP.NET (9 z 10)

Autor : Tom Dykstra

Przykładowa aplikacja internetowa Contoso University pokazuje, jak utworzyć aplikacje ASP.NET MVC 4 przy użyciu programu Entity Framework 5 Code First i Visual Studio 2012. Aby uzyskać informacje na temat serii samouczków, zobacz pierwszy samouczek z serii.

Uwaga

Jeśli napotkasz problem, którego nie możesz rozwiązać, pobierz ukończony rozdział i spróbuj odtworzyć problem. Zazwyczaj rozwiązanie problemu można znaleźć, porównując kod z ukończonym kodem. Aby uzyskać informacje o niektórych typowych błędach i sposobach ich rozwiązywania, zobacz Błędy i obejścia.

W poprzednim samouczku użyto dziedziczenia w celu zmniejszenia nadmiarowego kodu w Student klasach jednostek i Instructor . W tym samouczku przedstawiono kilka sposobów używania repozytorium i jednostek wzorców pracy dla operacji CRUD. Podobnie jak w poprzednim samouczku, w tym samouczku zmienisz sposób działania kodu na stronach, które zostały już utworzone, zamiast tworzyć nowe strony.

Repozytorium i jednostka wzorców pracy

Repozytorium i jednostka wzorców pracy mają na celu utworzenie warstwy abstrakcji między warstwą dostępu do danych a warstwą logiki biznesowej aplikacji. Zaimplementowanie tych wzorców może pomóc odizolować aplikację od zmian w magazynie danych i ułatwić zautomatyzowane testowanie jednostkowe lub programowanie oparte na testach (TDD).

W tym samouczku zaimplementujesz klasę repozytorium dla każdego typu jednostki. Student Dla typu jednostki utworzysz interfejs repozytorium i klasę repozytorium. Po utworzeniu wystąpienia repozytorium w kontrolerze użyjesz interfejsu, aby kontroler akceptował odwołanie do dowolnego obiektu, który implementuje interfejs repozytorium. Gdy kontroler działa w ramach serwera internetowego, otrzymuje repozytorium, które współpracuje z programem Entity Framework. Gdy kontroler działa w ramach klasy testu jednostkowego, otrzymuje repozytorium, które działa z danymi przechowywanymi w sposób, który można łatwo manipulować do testowania, na przykład kolekcji w pamięci.

W dalszej części samouczka użyjesz wielu repozytoriów i jednostki klasy roboczej dla Course typów jednostek i Department w kontrolerze Course . Jednostka klasy roboczej koordynuje pracę wielu repozytoriów, tworząc pojedynczą klasę kontekstu bazy danych udostępnioną przez wszystkie z nich. Jeśli chcesz mieć możliwość przeprowadzenia zautomatyzowanego testowania jednostkowego, utworzysz interfejsy dla tych klas i użyjesz ich w taki sam sposób, jak w Student przypadku repozytorium. Jednak aby zachować prostotę samouczka, utworzysz i użyjesz tych klas bez interfejsów.

Poniższa ilustracja przedstawia jeden ze sposobów koncepcyjnych relacji między klasami kontrolera i kontekstu w porównaniu do braku użycia repozytorium lub jednostki wzorca pracy.

Repository_pattern_diagram

Nie utworzysz testów jednostkowych w tej serii samouczków. Aby zapoznać się z wprowadzeniem do usługi TDD z aplikacją MVC korzystającą ze wzorca repozytorium, zobacz Przewodnik: używanie funkcji TDD z ASP.NET MVC. Aby uzyskać więcej informacji na temat wzorca repozytorium, zobacz następujące zasoby:

Uwaga

Istnieje wiele sposobów implementowania repozytorium i jednostki wzorców pracy. Klasy repozytorium można używać z jednostką klasy roboczej lub bez niego. Można zaimplementować pojedyncze repozytorium dla wszystkich typów jednostek lub jedno dla każdego typu. W przypadku zaimplementowania jednego dla każdego typu można użyć oddzielnych klas, ogólnej klasy bazowej i klas pochodnych albo abstrakcyjnej klasy bazowej i klas pochodnych. Logikę biznesową można uwzględnić w repozytorium lub ograniczyć ją do logiki dostępu do danych. Warstwę abstrakcji można również utworzyć w klasie kontekstowej bazy danych przy użyciu interfejsów IDbSet zamiast typów dbSet dla zestawów jednostek. Podejście do implementowania warstwy abstrakcji pokazanej w tym samouczku jest jedną z opcji, które należy wziąć pod uwagę, a nie zalecenia dla wszystkich scenariuszy i środowisk.

Tworzenie klasy repozytorium uczniów

W folderze DAL utwórz plik klasy o nazwie IStudentRepository.cs i zastąp istniejący kod następującym kodem:

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

Ten kod deklaruje typowy zestaw metod CRUD, w tym dwie metody odczytu — jedną zwracającą wszystkie Student jednostki i jedną, która znajduje pojedynczą Student jednostkę według identyfikatora.

W folderze DAL utwórz plik klasy o nazwie StudentRepository.cs . Zastąp istniejący kod następującym kodem, który implementuje IStudentRepository interfejs:

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

Kontekst bazy danych jest zdefiniowany w zmiennej klasy, a konstruktor oczekuje, że obiekt wywołujący zostanie przekazany w wystąpieniu kontekstu:

private SchoolContext context;

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

Możesz utworzyć wystąpienie nowego kontekstu w repozytorium, ale jeśli użyto wielu repozytoriów w jednym kontrolerze, każdy z nich będzie miał osobny kontekst. Później będziesz używać wielu repozytoriów w kontrolerze Course i zobaczysz, jak jednostka klasy roboczej może zapewnić, że wszystkie repozytoria używają tego samego kontekstu.

Repozytorium implementuje interfejs IDisposable i usuwa kontekst bazy danych, jak pokazano wcześniej w kontrolerze, a jego metody CRUD tworzą wywołania kontekstu bazy danych w taki sam sposób, jak pokazano wcześniej.

Zmienianie kontrolera ucznia na korzystanie z repozytorium

W pliku StudentController.cs zastąp kod aktualnie w klasie następującym kodem. Zmiany są wyróżnione.

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

Kontroler deklaruje teraz zmienną klasy dla obiektu, który implementuje IStudentRepository interfejs zamiast klasy kontekstu:

private IStudentRepository studentRepository;

Konstruktor domyślny (bez parametrów) tworzy nowe wystąpienie kontekstu, a opcjonalny konstruktor umożliwia obiekt wywołujący przekazywanie w wystąpieniu kontekstu.

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

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

(Jeśli używasz iniekcji zależności lub di, nie potrzebujesz konstruktora domyślnego, ponieważ oprogramowanie DI zapewni, że prawidłowy obiekt repozytorium będzie zawsze udostępniany).

W metodach CRUD repozytorium jest teraz wywoływane zamiast kontekstu:

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 Metoda usuwa teraz repozytorium zamiast kontekstu:

studentRepository.Dispose();

Uruchom witrynę i kliknij kartę Uczniowie .

Students_Index_page

Strona wygląda i działa tak samo jak przed zmianą kodu w celu korzystania z repozytorium, a inne strony uczniów również działają tak samo. Jednak istnieje ważna różnica w sposobie, w jaki Index metoda kontrolera wykonuje filtrowanie i porządkowanie. Oryginalna wersja tej metody zawierała następujący kod:

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

Zaktualizowana Index metoda zawiera następujący kod:

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

Zmieniono tylko wyróżniony kod.

W oryginalnej wersji kodu students jest wpisywany jako IQueryable obiekt. Zapytanie nie jest wysyłane do bazy danych, dopóki nie zostanie przekonwertowane na kolekcję przy użyciu metody, takiej jak ToList, która nie występuje, dopóki widok indeksu nie uzyskuje dostępu do modelu ucznia. Metoda Where w powyższym oryginalnym kodzie staje się klauzulą WHERE w zapytaniu SQL wysyłanym do bazy danych. Oznacza to z kolei, że tylko wybrane jednostki są zwracane przez bazę danych. Jednak w wyniku zmiany context.Students na studentRepository.GetStudents()students zmienną , zmienna po tej instrukcji jest kolekcją zawierającą IEnumerable wszystkich uczniów w bazie danych. Końcowy wynik zastosowania Where metody jest taki sam, ale teraz praca jest wykonywana w pamięci na serwerze internetowym, a nie przez bazę danych. W przypadku zapytań, które zwracają duże ilości danych, może to być nieefektywne.

Porada

IQueryable a IEnumerable

Po zaimplementowaniu repozytorium, jak pokazano tutaj, nawet jeśli wprowadzisz coś w polu wyszukiwania zapytanie wysłane do SQL Server zwróci wszystkie wiersze Ucznia, ponieważ nie zawiera kryteriów wyszukiwania:

Zrzut ekranu przedstawiający kod przedstawiający zaimplementowane i wyróżnione nowe repozytorium uczniów.

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'

To zapytanie zwraca wszystkie dane uczniów, ponieważ repozytorium wykonało zapytanie bez znajomości kryteriów wyszukiwania. Proces sortowania, stosowania kryteriów wyszukiwania i wybierania podzestawu danych do stronicowania (pokazującego tylko 3 wiersze w tym przypadku) odbywa się w pamięci później, gdy ToPagedList metoda jest wywoływana w IEnumerable kolekcji.

W poprzedniej wersji kodu (przed zaimplementowaniem repozytorium) zapytanie nie jest wysyłane do bazy danych do momentu zastosowania kryteriów wyszukiwania po ToPagedList wywołaniu IQueryable obiektu.

Zrzut ekranu przedstawiający kod kontrolera ucznia. Wyróżniono wiersz ciągu wyszukiwania kodu i wiersz listy do stronicowania kodu.

Gdy element ToPagedList jest wywoływany w IQueryable obiekcie, zapytanie wysyłane do SQL Server określa ciąg wyszukiwania, a w wyniku zwracane są tylko wiersze spełniające kryteria wyszukiwania i nie trzeba filtrować w pamięci.

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'

(W poniższym samouczku wyjaśniono, jak badać zapytania wysyłane do SQL Server).

W poniższej sekcji pokazano, jak zaimplementować metody repozytorium, które umożliwiają określenie, że ta praca powinna być wykonywana przez bazę danych.

Utworzono warstwę abstrakcji między kontrolerem a kontekstem bazy danych programu Entity Framework. Jeśli zamierzasz przeprowadzić zautomatyzowane testowanie jednostkowe za pomocą tej aplikacji, możesz utworzyć alternatywną klasę repozytorium w projekcie testu jednostkowego, który implementuje IStudentRepositoryprogram . Zamiast wywoływać kontekst do odczytu i zapisu danych, ta wyśmiewa klasa repozytorium może manipulować kolekcjami w pamięci w celu przetestowania funkcji kontrolera.

Implementowanie ogólnego repozytorium i jednostki klasy roboczej

Utworzenie klasy repozytorium dla każdego typu jednostki może spowodować powstanie dużej ilości nadmiarowego kodu i może spowodować częściowe aktualizacje. Załóżmy na przykład, że musisz zaktualizować dwa różne typy jednostek w ramach tej samej transakcji. Jeśli każde z nich korzysta z oddzielnego wystąpienia kontekstu bazy danych, może się powieść, a druga może zakończyć się niepowodzeniem. Jednym ze sposobów zminimalizowania nadmiarowego kodu jest użycie repozytorium ogólnego, a jednym ze sposobów zapewnienia, że wszystkie repozytoria używają tego samego kontekstu bazy danych (a tym samym koordynują wszystkie aktualizacje) jest użycie jednostki klasy roboczej.

W tej sekcji samouczka utworzysz klasę i klasę GenericRepositoryUnitOfWork , a następnie użyjesz ich w kontrolerze Course , aby uzyskać dostęp zarówno Department do zestawów jednostek, jak i Course zestawów jednostek. Jak wyjaśniono wcześniej, aby zachować prostą część tego samouczka, nie tworzysz interfejsów dla tych klas. Ale jeśli zamierzasz ich używać do ułatwienia TDD, zazwyczaj implementujesz je z interfejsami w taki sam sposób, jak w Student repozytorium.

Tworzenie repozytorium ogólnego

W folderze DAL utwórz plik GenericRepository.cs i zastąp istniejący kod następującym kodem:

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

Zmienne klasy są deklarowane dla kontekstu bazy danych i dla zestawu jednostek, dla którego utworzono wystąpienie repozytorium:

internal SchoolContext context;
internal DbSet dbSet;

Konstruktor akceptuje wystąpienie kontekstu bazy danych i inicjuje zmienną zestawu jednostek:

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

Metoda Get używa wyrażeń lambda, aby umożliwić kodowi wywołującego określenie warunku filtru i kolumny w celu uporządkowania wyników, a parametr ciągu umożliwia obiektowi wywołującego podanie rozdzielanej przecinkami listy właściwości nawigacji na potrzeby chętnego ładowania:

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

Kod Expression<Func<TEntity, bool>> filter oznacza, że obiekt wywołujący będzie dostarczać wyrażenie lambda na TEntity podstawie typu, a to wyrażenie zwróci wartość logiczną. Jeśli na przykład repozytorium jest tworzone dla Student typu jednostki, kod w metodzie wywołującej może określać student => student.LastName == "Smithwartość " dla parametru filter .

Kod Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy oznacza również, że obiekt wywołujący zapewni wyrażenie lambda. Jednak w tym przypadku dane wejściowe wyrażenia są obiektem IQueryable typu TEntity . Wyrażenie zwróci uporządkowaną wersję tego IQueryable obiektu. Jeśli na przykład repozytorium jest tworzone dla Student typu jednostki, kod w metodzie wywołującej może określać q => q.OrderBy(s => s.LastName) parametr orderBy .

Kod w metodzie Get tworzy IQueryable obiekt, a następnie stosuje wyrażenie filtru, jeśli istnieje:

IQueryable<TEntity> query = dbSet;

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

Następnie zastosuje wyrażenia chętne do ładowania po przeanalizowaniu listy rozdzielanej przecinkami:

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

Na koniec wyrażenie jest stosowane orderBy , jeśli istnieje i zwraca wyniki. W przeciwnym razie zwraca wyniki z zapytania nieurządkowanego:

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

Podczas wywoływania Get metody można przeprowadzić filtrowanie i sortowanie IEnumerable kolekcji zwracanej przez metodę zamiast podawania parametrów dla tych funkcji. Jednak sortowanie i filtrowanie będzie wykonywane w pamięci na serwerze internetowym. Korzystając z tych parametrów, upewnij się, że praca jest wykonywana przez bazę danych, a nie przez serwer internetowy. Alternatywą jest utworzenie klas pochodnych dla określonych typów jednostek i dodanie wyspecjalizowanych Get metod, takich jak GetStudentsInNameOrder lub GetStudentsByName. Jednak w złożonej aplikacji może to spowodować dużą liczbę takich klas pochodnych i wyspecjalizowanych metod, które mogą być bardziej pracowite w utrzymaniu.

Kod w metodach GetByID, Inserti Update jest podobny do tego, co zostało wyświetlone w repozytorium niegeneryjnym. (Nie udostępniasz chętnego parametru ładowania w podpisie GetByID , ponieważ nie można wykonać chętnego ładowania za Find pomocą metody ).

Dla metody podano Delete dwa przeciążenia:

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

Jeden z tych elementów umożliwia przekazanie tylko identyfikatora jednostki do usunięcia, a jeden przyjmuje wystąpienie jednostki. Jak pokazano w samouczku Dotyczącym współbieżności, do obsługi współbieżności potrzebna jest Delete metoda, która przyjmuje wystąpienie jednostki zawierające oryginalną wartość właściwości śledzenia.

To ogólne repozytorium będzie obsługiwać typowe wymagania CRUD. Jeśli określony typ jednostki ma specjalne wymagania, takie jak bardziej złożone filtrowanie lub porządkowanie, można utworzyć klasę pochodną, która ma dodatkowe metody dla tego typu.

Tworzenie jednostki klasy pracy

Jednostka klasy roboczej służy jednemu celowi: aby upewnić się, że w przypadku korzystania z wielu repozytoriów współużytkują one pojedynczy kontekst bazy danych. W ten sposób po zakończeniu jednostki pracy można wywołać SaveChanges metodę dla tego wystąpienia kontekstu i mieć pewność, że wszystkie powiązane zmiany zostaną skoordynowane. Wszystko, co wymaga klasy, to Save metoda i właściwość dla każdego repozytorium. Każda właściwość repozytorium zwraca wystąpienie repozytorium, które zostało utworzone przy użyciu tego samego wystąpienia kontekstu bazy danych co inne wystąpienia repozytorium.

W folderze DAL utwórz plik klasy o nazwie UnitOfWork.cs i zastąp kod szablonu następującym kodem:

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

Kod tworzy zmienne klasy dla kontekstu bazy danych i każdego repozytorium. W przypadku zmiennej context tworzone jest nowe wystąpienie kontekstu:

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

Każda właściwość repozytorium sprawdza, czy repozytorium już istnieje. Jeśli nie, tworzy wystąpienie repozytorium, przekazując wystąpienie kontekstu. W związku z tym wszystkie repozytoria współdzielą to samo wystąpienie kontekstu.

public GenericRepository<Department> DepartmentRepository
{
    get
    {

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

Metoda Save wywołuje SaveChanges kontekst bazy danych.

Podobnie jak każda klasa, która tworzy wystąpienie kontekstu bazy danych w zmiennej klasy, UnitOfWork klasa implementuje IDisposable i usuwa kontekst.

Zmiana kontrolera kursu w celu używania klas UnitOfWork i repozytoriów

Zastąp kod aktualnie dostępny w pliku CourseController.cs następującym kodem:

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

Ten kod dodaje zmienną klasy dla UnitOfWork klasy. (Jeśli w tym miejscu używasz interfejsów, nie zainicjuj tutaj zmiennej. Zamiast tego należy zaimplementować wzorzec dwóch konstruktorów tak samo jak w Student przypadku repozytorium).

private UnitOfWork unitOfWork = new UnitOfWork();

W pozostałej części klasy wszystkie odwołania do kontekstu bazy danych są zastępowane odwołaniami do odpowiedniego repozytorium przy użyciu UnitOfWork właściwości w celu uzyskania dostępu do repozytorium. Metoda Dispose usuwa UnitOfWork wystąpienie.

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

Uruchom witrynę i kliknij kartę Kursy .

Courses_Index_page

Strona wygląda i działa tak samo jak przed zmianami, a inne strony Kursu również działają tak samo.

Podsumowanie

Zaimplementowano teraz zarówno repozytorium, jak i jednostkę wzorców pracy. W repozytorium ogólnym użyto wyrażeń lambda jako parametrów metody. Aby uzyskać więcej informacji na temat używania tych wyrażeń z obiektem IQueryable , zobacz Interfejs IQueryable(T) (System.Linq) w bibliotece MSDN. W następnym samouczku dowiesz się, jak obsługiwać niektóre zaawansowane scenariusze.

Linki do innych zasobów programu Entity Framework można znaleźć na mapie zawartości dostępu do danych ASP.NET.