Implementacja wzorców repozytorium i jednostki pracy w aplikacji ASP.NET MVC (9 z 10)Implementing the Repository and Unit of Work Patterns in an ASP.NET MVC Application (9 of 10)
Autor Dykstraby Tom Dykstra
Pobierz ukończony projektDownload Completed Project
Przykładowa aplikacja internetowa Contoso University pokazuje, jak tworzyć aplikacje ASP.NET MVC 4 przy użyciu Entity Framework 5 Code First i programu Visual Studio 2012.The Contoso University sample web application demonstrates how to create ASP.NET MVC 4 applications using the Entity Framework 5 Code First and Visual Studio 2012. Aby uzyskać informacje na temat serii samouczków, zobacz pierwszy samouczek w serii.For information about the tutorial series, see the first tutorial in the series. Możesz uruchomić serię samouczków od początku lub pobrać początkowy projekt dla tego rozdziału i Zacznij tutaj.You can start the tutorial series from the beginning or download a starter project for this chapter and start here.
Note
Jeśli wystąpi problem, którego nie można rozwiązać, Pobierz ukończony rozdział i spróbuj odtworzyć problem.If you run into a problem you can't resolve, download the completed chapter and try to reproduce your problem. Ogólnie rzecz biorąc, można znaleźć rozwiązanie problemu, porównując kod z kompletnym kodem.You can generally find the solution to the problem by comparing your code to the completed code. Niektóre typowe błędy i sposoby ich rozwiązywania można znaleźć w temacie błędy i obejścia.For some common errors and how to solve them, see Errors and Workarounds.
W poprzednim samouczku użyto dziedziczenia do zredukowania nadmiarowego kodu w Student
i Instructor
klas jednostek.In the previous tutorial you used inheritance to reduce redundant code in the Student
and Instructor
entity classes. W tym samouczku przedstawiono kilka sposobów używania wzorców repozytorium i jednostki pracy dla operacji CRUD.In this tutorial you'll see some ways to use the repository and unit of work patterns for CRUD operations. Tak jak w poprzednim samouczku, w tym kroku zmienimy sposób, w jaki kod będzie działać ze stronami, które zostały już utworzone, zamiast tworzyć nowe strony.As in the previous tutorial, in this one you'll change the way your code works with pages you already created rather than creating new pages.
Wzorce repozytorium i jednostki pracyThe Repository and Unit of Work Patterns
Wzorce repozytorium i jednostki pracy są przeznaczone do tworzenia warstwy abstrakcji między warstwą dostępu do danych i warstwą logiki biznesowej aplikacji.The repository and unit of work patterns are intended to create an abstraction layer between the data access layer and the business logic layer of an application. Wdrożenie tych wzorców może pomóc w izolowaniu aplikacji od zmian w magazynie danych oraz w celu ułatwienia zautomatyzowanych testów jednostkowych lub projektowania opartego na testach (TDD).Implementing these patterns can help insulate your application from changes in the data store and can facilitate automated unit testing or test-driven development (TDD).
W tym samouczku zaimplementowano klasę repozytorium dla każdego typu jednostki.In this tutorial you'll implement a repository class for each entity type. Dla typu jednostki Student
utworzysz interfejs repozytorium i klasę repozytorium.For the Student
entity type you'll create a repository interface and a repository class. Gdy tworzysz wystąpienie repozytorium w kontrolerze, użyjesz interfejsu, aby kontroler zaakceptuje odwołanie do dowolnego obiektu, który implementuje interfejs repozytorium.When you instantiate the repository in your controller, you'll use the interface so that the controller will accept a reference to any object that implements the repository interface. Gdy kontroler działa na serwerze sieci Web, odbiera repozytorium, które działa z Entity Framework.When the controller runs under a web server, it receives a repository that works with the Entity Framework. Gdy kontroler jest uruchamiany w klasie testów jednostkowych, odbiera repozytorium, które działa z danymi przechowywanymi w sposób, który można łatwo manipulować na potrzeby testowania, takich jak kolekcja w pamięci.When the controller runs under a unit test class, it receives a repository that works with data stored in a way that you can easily manipulate for testing, such as an in-memory collection.
W dalszej części tego samouczka użyjesz wielu repozytoriów i klasy jednostki pracy dla Course
i Department
typów jednostek na kontrolerze Course
.Later in the tutorial you'll use multiple repositories and a unit of work class for the Course
and Department
entity types in the Course
controller. Jednostka klasy Work koordynuje prace wielu repozytoriów przez utworzenie pojedynczej klasy kontekstu bazy danych udostępnionej przez wszystkie.The unit of work class coordinates the work of multiple repositories by creating a single database context class shared by all of them. Jeśli chcesz mieć możliwość wykonywania zautomatyzowanych testów jednostkowych, Utwórz interfejsy dla tych klas i używaj ich w taki sam sposób, jak w przypadku repozytorium Student
.If you wanted to be able to perform automated unit testing, you'd create and use interfaces for these classes in the same way you did for the Student
repository. Aby jednak zachować ten samouczek, możesz tworzyć i używać tych klas bez interfejsów.However, to keep the tutorial simple, you'll create and use these classes without interfaces.
Na poniższej ilustracji przedstawiono jeden ze sposobów konceptualizacji relacji między klasą kontrolera i kontekstu w porównaniu z nieużywanym wzorcem wzorca lub jednostki pracy.The following illustration shows one way to conceptualize the relationships between the controller and context classes compared to not using the repository or unit of work pattern at all.
Nie utworzysz testów jednostkowych w tej serii samouczków.You won't create unit tests in this tutorial series. Aby zapoznać się z wprowadzeniem do programu TDD z aplikacją MVC korzystającą ze wzorca repozytorium, zobacz Przewodnik: korzystanie z usługi TDD z ASP.NET MVC.For an introduction to TDD with an MVC application that uses the repository pattern, see Walkthrough: Using TDD with ASP.NET MVC. Aby uzyskać więcej informacji na temat wzorca repozytorium, zobacz następujące zasoby:For more information about the repository pattern, see the following resources:
- Wzorzec repozytorium w witrynie MSDN.The Repository Pattern on MSDN.
- Używanie wzorców repozytorium i jednostki pracy z Entity Framework 4,0 na blogu Entity Framework zespołu.Using Repository and Unit of Work patterns with Entity Framework 4.0 on the Entity Framework team blog.
- Agile Entity Framework 4 repozytorium wpisów w blogu Julie Lerman.Agile Entity Framework 4 Repository series of posts on Julie Lerman's blog.
- Tworzenie konta z błyskawiczną aplikacją HTML5/jQuery na blogu Dan Wahlin.Building the Account at a Glance HTML5/jQuery Application on Dan Wahlin's blog.
Note
Istnieje wiele sposobów implementacji wzorców repozytorium i jednostki pracy.There are many ways to implement the repository and unit of work patterns. Można użyć klas repozytorium z klasą jednostki pracy lub bez niej.You can use repository classes with or without a unit of work class. Można zaimplementować pojedyncze repozytorium dla wszystkich typów jednostek lub jeden dla każdego typu.You can implement a single repository for all entity types, or one for each type. Jeśli zaimplementowano jeden dla każdego typu, można użyć oddzielnych klas, ogólnej klasy bazowej i klas pochodnych lub abstrakcyjnej klasy bazowej i klas pochodnych.If you implement one for each type, you can use separate classes, a generic base class and derived classes, or an abstract base class and derived classes. W repozytorium można uwzględnić logikę biznesową lub ograniczyć ją do logiki dostępu do danych.You can include business logic in your repository or restrict it to data access logic. Możesz również utworzyć warstwę abstrakcji w klasie kontekstu bazy danych przy użyciu interfejsów IDbSet zamiast typów nieogólnymi dla zestawów jednostek.You can also build an abstraction layer into your database context class by using IDbSet interfaces there instead of DbSet types for your entity sets. Podejście do implementowania warstwy abstrakcji pokazanej w tym samouczku to jedna z opcji, które należy wziąć pod uwagę, a nie na wszystkie scenariusze i środowiska.The approach to implementing an abstraction layer shown in this tutorial is one option for you to consider, not a recommendation for all scenarios and environments.
Tworzenie klasy repozytorium uczniówCreating the Student Repository Class
W folderze dal Utwórz plik klasy o nazwie IStudentRepository.cs i Zastąp istniejący kod następującym kodem:In the DAL folder, create a class file named IStudentRepository.cs and replace the existing code with the following code:
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 — jeden, który zwraca wszystkie jednostki Student
, a drugi, który znajduje pojedyncze Student
jednostki według identyfikatora.This code declares a typical set of CRUD methods, including two read methods — one that returns all Student
entities, and one that finds a single Student
entity by ID.
W folderze dal Utwórz plik klasy o nazwie StudentRepository.cs .In the DAL folder, create a class file named StudentRepository.cs file. Zastąp istniejący kod następującym kodem, który implementuje interfejs IStudentRepository
:Replace the existing code with the following code, which implements the IStudentRepository
interface:
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 do wystąpienia kontekstu:The database context is defined in a class variable, and the constructor expects the calling object to pass in an instance of the context:
private SchoolContext context;
public StudentRepository(SchoolContext context)
{
this.context = context;
}
Można utworzyć wystąpienie nowego kontekstu w repozytorium, ale jeśli w jednym kontrolerze użyto wielu repozytoriów, każda z nich będzie kończyć się niezależnym kontekstem.You could instantiate a new context in the repository, but then if you used multiple repositories in one controller, each would end up with a separate context. Później będziesz używać wielu repozytoriów w kontrolerze Course
i zobaczysz, jak jednostka klasy pracy może zapewnić, że wszystkie repozytoria używają tego samego kontekstu.Later you'll use multiple repositories in the Course
controller, and you'll see how a unit of work class can ensure that all repositories use the same context.
Repozytorium implementuje interfejs IDisposable i usuwa kontekst bazy danych zgodnie z wcześniejszym opisem w kontrolerze, a jego metody CRUD umożliwiają wywoływanie kontekstu bazy danych w taki sam sposób, jak wcześniej.The repository implements IDisposable and disposes the database context as you saw earlier in the controller, and its CRUD methods make calls to the database context in the same way that you saw earlier.
Zmień kontroler ucznia, aby korzystał z repozytoriumChange the Student Controller to Use the Repository
W StudentController.csZastąp kod aktualnie w klasie następującym kodem.In StudentController.cs, replace the code currently in the class with the following code. Zmiany są wyróżnione.The changes are highlighted.
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 interfejs IStudentRepository
zamiast klasy kontekstu:The controller now declares a class variable for an object that implements the IStudentRepository
interface instead of the context class:
private IStudentRepository studentRepository;
Domyślny Konstruktor (bez parametrów) tworzy nowe wystąpienie kontekstu, a opcjonalny Konstruktor zezwala obiektowi wywołującemu na przekazywanie w wystąpieniu kontekstu.The default (parameterless) constructor creates a new context instance, and an optional constructor allows the caller to pass in a context instance.
public StudentController()
{
this.studentRepository = new StudentRepository(new SchoolContext());
}
public StudentController(IStudentRepository studentRepository)
{
this.studentRepository = studentRepository;
}
(Jeśli korzystasz z iniekcji zależnościlub di, nie potrzebujesz domyślnego konstruktora, ponieważ oprogramowanie to zapewnia, że zawsze będzie zapewniony poprawny obiekt repozytorium).(If you were using dependency injection, or DI, you wouldn't need the default constructor because the DI software would ensure that the correct repository object would always be provided.)
W metodach CRUD repozytorium jest teraz wywoływane zamiast kontekstu:In the CRUD methods, the repository is now called instead of the context:
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();
A metoda Dispose
teraz usuwa repozytorium zamiast kontekstu:And the Dispose
method now disposes the repository instead of the context:
studentRepository.Dispose();
Uruchom witrynę i kliknij kartę uczniowie .Run the site and click the Students tab.
Strona wygląda i działa tak samo, jak przed zmianą kodu w celu użycia repozytorium, a inne strony ucznia również działają tak samo.The page looks and works the same as it did before you changed the code to use the repository, and the other Student pages also work the same. Istnieje jednak ważna różnica w sposobie filtrowania i porządkowania metody Index
kontrolera.However, there's an important difference in the way the Index
method of the controller does filtering and ordering. Oryginalna wersja tej metody zawierała następujący kod:The original version of this method contained the following code:
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 Metoda Index
zawiera następujący kod:The updated Index
method contains the following code:
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()));
}
Tylko wyróżniony kod został zmieniony.Only the highlighted code has changed.
W pierwotnej wersji kodu students
jest wpisana jako obiekt IQueryable
.In the original version of the code, students
is typed as an IQueryable
object. Zapytanie nie jest wysyłane do bazy danych, dopóki nie zostanie ono przekonwertowane do kolekcji przy użyciu metody takiej jak ToList
, która nie występuje do momentu, gdy widok indeksu uzyskuje dostęp do modelu ucznia.The query isn't sent to the database until it's converted into a collection using a method such as ToList
, which doesn't occur until the Index view accesses the student model. Metoda Where
w oryginalnym kodzie zostanie WHERE
klauzulą kwerendy SQL, która jest wysyłana do bazy danych.The Where
method in the original code above becomes a WHERE
clause in the SQL query that is sent to the database. To z kolei oznacza, że baza danych zwraca tylko wybrane jednostki.That in turn means that only the selected entities are returned by the database. Jednak w wyniku zmiany context.Students
na studentRepository.GetStudents()
, zmienna students
po tej instrukcji jest kolekcją IEnumerable
, która obejmuje wszystkich uczniów w bazie danych.However, as a result of changing context.Students
to studentRepository.GetStudents()
, the students
variable after this statement is an IEnumerable
collection that includes all students in the database. Rezultat zastosowania metody Where
jest taki sam, ale teraz prace odbywają się w pamięci na serwerze sieci Web, a nie w bazie danych programu.The end result of applying the Where
method is the same, but now the work is done in memory on the web server and not by the database. W przypadku zapytań, które zwracają duże ilości danych, może to być niewydajne.For queries that return large volumes of data, this can be inefficient.
Tip
IQueryable a IEnumerableIQueryable vs. IEnumerable
Po zaimplementowaniu repozytorium, jak pokazano tutaj, nawet jeśli wprowadzisz coś w polu wyszukiwania , zapytanie wysyłane do SQL Server zwraca wszystkie wiersze ucznia, ponieważ nie zawiera on kryteriów wyszukiwania:After you implement the repository as shown here, even if you enter something in the Search box the query sent to SQL Server returns all Student rows because it doesn't include your search criteria:
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 ucznia, ponieważ repozytorium wykonała zapytanie bez znajomości kryteriów wyszukiwania.This query returns all of the student data because the repository executed the query without knowing about the search criteria. Proces sortowania, stosowania kryteriów wyszukiwania i wybierania podzestawu danych na potrzeby stronicowania (wyświetlanie tylko 3 wierszy w tym przypadku) odbywa się później, gdy metoda ToPagedList
jest wywoływana w kolekcji IEnumerable
.The process of sorting, applying search criteria, and selecting a subset of the data for paging (showing only 3 rows in this case) is done in memory later when the ToPagedList
method is called on the IEnumerable
collection.
W poprzedniej wersji kodu (przed zaimplementowaniem repozytorium) zapytanie nie jest wysyłane do bazy danych do momentu zastosowania kryteriów wyszukiwania, gdy ToPagedList
jest wywoływana dla IQueryable
obiektu.In the previous version of the code (before you implemented the repository), the query is not sent to the database until after you apply the search criteria, when ToPagedList
is called on the IQueryable
object.
Gdy ToPagedList jest wywoływana dla obiektu IQueryable
, zapytanie wysyłane do SQL Server określa ciąg wyszukiwania, a jako wynik są zwracane tylko wiersze, które spełniają kryteria wyszukiwania, i nie trzeba wykonywać filtrowania w pamięci.When ToPagedList is called on an IQueryable
object, the query sent to SQL Server specifies the search string, and as a result only rows that meet the search criteria are returned, and no filtering needs to be done in memory.
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 sprawdzać zapytania wysyłane do SQL Server).(The following tutorial explains how to examine queries sent to SQL Server.)
W poniższej sekcji pokazano, jak zaimplementować metody repozytorium, które umożliwiają określenie, że ta czynność powinna zostać wykonana przez bazę danych.The following section shows how to implement repository methods that enable you to specify that this work should be done by the database.
Warstwa abstrakcji została teraz utworzona między kontrolerem a kontekstem bazy danych Entity Framework.You've now created an abstraction layer between the controller and the Entity Framework database context. Jeśli chcesz przeprowadzić zautomatyzowane testowanie jednostkowe za pomocą tej aplikacji, możesz utworzyć alternatywną klasę repozytorium w projekcie testów jednostkowych, który implementuje IStudentRepository
.If you were going to perform automated unit testing with this application, you could create an alternative repository class in a unit test project that implements IStudentRepository
. Zamiast wywołania kontekstu do odczytu i zapisu danych, ta klasa repozytorium może manipulować kolekcjami w pamięci w celu przetestowania funkcji kontrolera.Instead of calling the context to read and write data, this mock repository class could manipulate in-memory collections in order to test controller functions.
Implementowanie repozytorium ogólnego i klasy jednostki pracyImplement a Generic Repository and a Unit of Work Class
Utworzenie klasy repozytorium dla każdego typu jednostki może spowodować powstanie wielu nadmiarowego kodu i może spowodować częściowe aktualizacje.Creating a repository class for each entity type could result in a lot of redundant code, and it could result in partial updates. Załóżmy na przykład, że trzeba zaktualizować dwa różne typy jednostek w ramach tej samej transakcji.For example, suppose you have to update two different entity types as part of the same transaction. Jeśli każda z nich używa oddzielnego wystąpienia kontekstu bazy danych, jeden może się powieść, a druga może się nie powieść.If each uses a separate database context instance, one might succeed and the other might fail. Jednym ze sposobów minimalizowania nadmiarowego kodu jest użycie ogólnego repozytorium, a jednym ze sposobów zapewnienia, że wszystkie repozytoria używają tego samego kontekstu bazy danych (i w związku z tym koordynują wszystkie aktualizacje), ma używać jednostki klasy pracy.One way to minimize redundant code is to use a generic repository, and one way to ensure that all repositories use the same database context (and thus coordinate all updates) is to use a unit of work class.
W tej części samouczka utworzysz klasę GenericRepository
i klasę UnitOfWork
, a następnie użyjesz ich na kontrolerze Course
, aby uzyskać dostęp do zestawów jednostek Department
i Course
.In this section of the tutorial, you'll create a GenericRepository
class and a UnitOfWork
class, and use them in the Course
controller to access both the Department
and the Course
entity sets. Jak wyjaśniono wcześniej, aby ta część samouczka była prosta, nie tworzysz interfejsów dla tych klas.As explained earlier, to keep this part of the tutorial simple, you aren't creating interfaces for these classes. Ale jeśli zamierzasz korzystać z nich w celu ułatwienia korzystania z nich, zazwyczaj Wdrażaj je przy użyciu interfejsów w taki sam sposób, jak repozytorium Student
.But if you were going to use them to facilitate TDD, you'd typically implement them with interfaces the same way you did the Student
repository.
Tworzenie repozytorium ogólnegoCreate a Generic Repository
W folderze dal Utwórz GenericRepository.cs i Zastąp istniejący kod następującym kodem:In the DAL folder, create GenericRepository.cs and replace the existing code with the following code:
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 jest tworzone wystąpienie repozytorium:Class variables are declared for the database context and for the entity set that the repository is instantiated for:
internal SchoolContext context;
internal DbSet dbSet;
Konstruktor akceptuje wystąpienie kontekstu bazy danych i inicjuje zmienną zestawu jednostek:The constructor accepts a database context instance and initializes the entity set variable:
public GenericRepository(SchoolContext context)
{
this.context = context;
this.dbSet = context.Set<TEntity>();
}
Metoda Get
używa wyrażeń lambda, aby umożliwić kod wywołujący, aby określić warunek filtru i kolumnę, w której mają być uporządkowane wyniki, a parametr String umożliwia obiektowi wywołującemu przeznaczenie listy właściwości nawigacji do eager ładowania:The Get
method uses lambda expressions to allow the calling code to specify a filter condition and a column to order the results by, and a string parameter lets the caller provide a comma-delimited list of navigation properties for eager loading:
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 dostarczy wyrażenie lambda na podstawie typu TEntity
, a to wyrażenie zwróci wartość logiczną.The code Expression<Func<TEntity, bool>> filter
means the caller will provide a lambda expression based on the TEntity
type, and this expression will return a Boolean value. Na przykład jeśli repozytorium jest tworzone dla typu jednostki Student
, kod w metodzie wywołującej może określić student => student.LastName == "Smith
" dla parametru filter
.For example, if the repository is instantiated for the Student
entity type, the code in the calling method might specify student => student.LastName == "Smith
" for the filter
parameter.
Kod Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy
oznacza również, że obiekt wywołujący dostarczy wyrażenie lambda.The code Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy
also means the caller will provide a lambda expression. Ale w tym przypadku dane wejściowe do wyrażenia są obiektem IQueryable
dla typu TEntity
.But in this case, the input to the expression is an IQueryable
object for the TEntity
type. Wyrażenie zwróci uporządkowaną wersję tego obiektu IQueryable
.The expression will return an ordered version of that IQueryable
object. Na przykład jeśli repozytorium jest tworzone dla typu jednostki Student
, kod w metodzie wywołującej może określić q => q.OrderBy(s => s.LastName)
parametru orderBy
.For example, if the repository is instantiated for the Student
entity type, the code in the calling method might specify q => q.OrderBy(s => s.LastName)
for the orderBy
parameter.
Kod w metodzie Get
tworzy obiekt IQueryable
, a następnie stosuje wyrażenie filtru, jeśli istnieje:The code in the Get
method creates an IQueryable
object and then applies the filter expression if there is one:
IQueryable<TEntity> query = dbSet;
if (filter != null)
{
query = query.Where(filter);
}
Następnie stosuje wyrażenia ładowania eager po przeanalizowaniu listy rozdzielanej przecinkami:Next it applies the eager-loading expressions after parsing the comma-delimited list:
foreach (var includeProperty in includeProperties.Split
(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty);
}
Na koniec stosuje wyrażenie orderBy
, jeśli istnieje i zwraca wyniki. w przeciwnym razie zwraca wyniki z kwerendy nieuporządkowanej:Finally, it applies the orderBy
expression if there is one and returns the results; otherwise it returns the results from the unordered query:
if (orderBy != null)
{
return orderBy(query).ToList();
}
else
{
return query.ToList();
}
Po wywołaniu metody Get
można filtrować i sortować według IEnumerable
j kolekcji zwróconej przez metodę zamiast dostarczać parametry dla tych funkcji.When you call the Get
method, you could do filtering and sorting on the IEnumerable
collection returned by the method instead of providing parameters for these functions. Jednak zadania sortowania i filtrowania są następnie wykonywane w pamięci na serwerze sieci Web.But the sorting and filtering work would then be done in memory on the web server. Korzystając z tych parametrów, należy się upewnić, że prace są wykonywane przez bazę danych, a nie na serwerze sieci Web.By using these parameters, you ensure that the work is done by the database rather than the web server. Alternatywą jest tworzenie klas pochodnych dla określonych typów jednostek i Dodawanie wyspecjalizowanych metod Get
, takich jak GetStudentsInNameOrder
lub GetStudentsByName
.An alternative is to create derived classes for specific entity types and add specialized Get
methods, such as GetStudentsInNameOrder
or GetStudentsByName
. Jednak w złożonej aplikacji może to spowodować powstanie dużej liczby takich klas pochodnych i wyspecjalizowanych metod, co może obsłużyć więcej pracy.However, in a complex application, this can result in a large number of such derived classes and specialized methods, which could be more work to maintain.
Kod w metodach GetByID
, Insert
i Update
jest podobny do tego, co zostało nadane w repozytorium nieogólnym.The code in the GetByID
, Insert
, and Update
methods is similar to what you saw in the non-generic repository. (W sygnaturze GetByID
nie jest podawany parametr ładowania eager, ponieważ nie można wykonać ładowania eager przy użyciu metody Find
).(You aren't providing an eager loading parameter in the GetByID
signature, because you can't do eager loading with the Find
method.)
Dla metody Delete
są dostępne dwa przeciążenia:Two overloads are provided for the Delete
method:
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 umożliwia przekazanie tylko identyfikatora jednostki do usunięcia, a jednym z nich jest wystąpienie jednostki.One of these lets you pass in just the ID of the entity to be deleted, and one takes an entity instance. Jak przedstawiono w samouczku Obsługa współbieżności , na potrzeby obsługi współbieżności potrzebujesz metody Delete
, która przyjmuje wystąpienie jednostki, które zawiera oryginalną wartość właściwości śledzenia.As you saw in the Handling Concurrency tutorial, for concurrency handling you need a Delete
method that takes an entity instance that includes the original value of a tracking property.
To ogólne repozytorium będzie obsługiwało typowe wymagania CRUD.This generic repository will handle typical CRUD requirements. 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.When a particular entity type has special requirements, such as more complex filtering or ordering, you can create a derived class that has additional methods for that type.
Tworzenie jednostki klasy pracyCreating the Unit of Work Class
Jednostka klasy pracy służy do jednego celu: aby upewnić się, że w przypadku korzystania z wielu repozytoriów, współużytkują one kontekst pojedynczej bazy danych.The unit of work class serves one purpose: to make sure that when you use multiple repositories, they share a single database context. W ten sposób, gdy jednostka pracy jest ukończona, można wywołać metodę SaveChanges
w tym wystąpieniu kontekstu i mieć pewność, że wszystkie powiązane zmiany zostaną skoordynowane.That way, when a unit of work is complete you can call the SaveChanges
method on that instance of the context and be assured that all related changes will be coordinated. Wszystko, co potrzebuje Klasa, jest metodą Save
i właściwością dla każdego repozytorium.All that the class needs is a Save
method and a property for each repository. 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.Each repository property returns a repository instance that has been instantiated using the same database context instance as the other repository instances.
W folderze dal Utwórz plik klasy o nazwie UnitOfWork.cs i Zastąp kod szablonu następującym kodem:In the DAL folder, create a class file named UnitOfWork.cs and replace the template code with the following code:
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 klas dla kontekstu bazy danych i każdego repozytorium.The code creates class variables for the database context and each repository. Dla zmiennej context
jest tworzone wystąpienie nowego kontekstu:For the context
variable, a new context is instantiated:
private SchoolContext context = new SchoolContext();
private GenericRepository<Department> departmentRepository;
private GenericRepository<Course> courseRepository;
Każda właściwość repozytorium sprawdza, czy repozytorium już istnieje.Each repository property checks whether the repository already exists. W przeciwnym razie tworzy wystąpienie repozytorium, przekazując w wystąpienie kontekstu.If not, it instantiates the repository, passing in the context instance. W związku z tym wszystkie repozytoria współużytkują to samo wystąpienie kontekstu.As a result, all repositories share the same context instance.
public GenericRepository<Department> DepartmentRepository
{
get
{
if (this.departmentRepository == null)
{
this.departmentRepository = new GenericRepository<Department>(context);
}
return departmentRepository;
}
}
Metoda Save
wywołuje SaveChanges
w kontekście bazy danych.The Save
method calls SaveChanges
on the database context.
Podobnie jak w przypadku każdej klasy, która tworzy wystąpienie kontekstu bazy danych w zmiennej klasy, Klasa UnitOfWork
implementuje IDisposable
i usuwa kontekst.Like any class that instantiates a database context in a class variable, the UnitOfWork
class implements IDisposable
and disposes the context.
Zmiana kontrolera kursu w celu używania klasy UnitOfWork i repozytoriówChanging the Course Controller to use the UnitOfWork Class and Repositories
Zastąp kod aktualnie w CourseController.cs następującym kodem:Replace the code you currently have in CourseController.cs with the following code:
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 klasy UnitOfWork
.This code adds a class variable for the UnitOfWork
class. (Jeśli w tym miejscu używasz interfejsów, nie można zainicjować zmiennej tutaj. zamiast tego należy zaimplementować wzorzec dwóch konstruktorów tak samo jak w przypadku repozytorium Student
.)(If you were using interfaces here, you wouldn't initialize the variable here; instead, you'd implement a pattern of two constructors just as you did for the Student
repository.)
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, aby uzyskać dostęp do repozytorium.In the rest of the class, all references to the database context are replaced by references to the appropriate repository, using UnitOfWork
properties to access the repository. Metoda Dispose
usuwa wystąpienie UnitOfWork
.The Dispose
method disposes the UnitOfWork
instance.
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 .Run the site and click the Courses tab.
Strona wygląda i działa tak samo jak przed zmianami, a inne strony kursu również działają tak samo.The page looks and works the same as it did before your changes, and the other Course pages also work the same.
PodsumowanieSummary
Wdrożono zarówno wzorce repozytorium, jak i jednostki pracy.You have now implemented both the repository and unit of work patterns. Użyto wyrażeń lambda jako parametrów metody w repozytorium ogólnym.You have used lambda expressions as method parameters in the generic repository. Aby uzyskać więcej informacji na temat używania tych wyrażeń z obiektem IQueryable
, zobacz interfejs IQueryable (t) Interface (System. LINQ) w bibliotece MSDN.For more information about how to use these expressions with an IQueryable
object, see IQueryable(T) Interface (System.Linq) in the MSDN Library. W następnym samouczku dowiesz się, jak obsługiwać niektóre zaawansowane scenariusze.In the next tutorial you'll learn how to handle some advanced scenarios.
Linki do innych zasobów Entity Framework można znaleźć w mapie zawartości dostęp do danych ASP.NET.Links to other Entity Framework resources can be found in the ASP.NET Data Access Content Map.