Implementieren des Repositorys und der Arbeitseinheitsmuster in einer ASP.NET MVC-Anwendung (9 von 10)
von Tom Dykstra
Die Contoso University-Beispielwebanwendung veranschaulicht, wie sie ASP.NET MVC 4-Anwendungen mithilfe von Entity Framework 5 Code First und Visual Studio 2012 erstellen. Informationen zu dieser Tutorialreihe finden Sie im ersten Tutorial der Reihe.
Hinweis
Wenn ein Problem auftritt, das Sie nicht beheben können, laden Sie das abgeschlossene Kapitel herunter , und versuchen Sie, das Problem zu reproduzieren. Im Allgemeinen können Sie die Lösung für das Problem finden, indem Sie Ihren Code mit dem abgeschlossenen Code vergleichen. Einige häufige Fehler und deren Behebung finden Sie unter Fehler und Problemumgehungen.
Im vorherigen Tutorial haben Sie die Vererbung verwendet, um redundanten Code in den Entitätsklassen Student
und Instructor
zu reduzieren. In diesem Tutorial erfahren Sie, wie Sie das Repository und die Arbeitseinheitsmuster für CRUD-Vorgänge verwenden können. Wie im vorherigen Tutorial ändern Sie in diesem Tutorial die Art und Weise, wie Ihr Code mit Bereits erstellten Seiten funktioniert, anstatt neue Seiten zu erstellen.
Das Repository und die Arbeitseinheitsmuster
Das Repository und die Arbeitseinheitsmuster sollen eine Abstraktionsebene zwischen der Datenzugriffsebene und der Geschäftslogikebene einer Anwendung erstellen. Die Implementierung dieser Muster unterstützt die Isolation Ihrer Anwendung vor Änderungen im Datenspeicher und kann automatisierte Komponententests oder eine testgesteuerte Entwicklung (Test-Driven Development, TDD) erleichtern.
In diesem Tutorial implementieren Sie eine Repositoryklasse für jeden Entitätstyp. Für den Student
Entitätstyp erstellen Sie eine Repositoryschnittstelle und eine Repositoryklasse. Wenn Sie das Repository in Ihrem Controller instanziieren, verwenden Sie die -Schnittstelle, damit der Controller einen Verweis auf jedes Objekt akzeptiert, das die Repositoryschnittstelle implementiert. Wenn der Controller unter einem Webserver ausgeführt wird, empfängt er ein Repository, das mit dem Entity Framework funktioniert. Wenn der Controller unter einer Komponententestklasse ausgeführt wird, empfängt er ein Repository, das mit Daten arbeitet, die auf eine Weise gespeichert sind, die Sie für Tests leicht bearbeiten können, z. B. eine In-Memory-Sammlung.
Später im Tutorial verwenden Sie mehrere Repositorys und eine Arbeitseinheit für die Course
Entitätstypen und Department
im Course
Controller. Die Arbeitseinheit koordiniert die Arbeit mehrerer Repositorys, indem eine einzelne Datenbankkontextklasse erstellt wird, die von allen gemeinsam genutzt wird. Wenn Sie automatisierte Komponententests durchführen möchten, würden Sie Schnittstellen für diese Klassen auf die gleiche Weise wie für das Student
Repository erstellen und verwenden. Um das Tutorial jedoch einfach zu halten, erstellen und verwenden Sie diese Klassen ohne Schnittstellen.
Die folgende Abbildung zeigt eine Möglichkeit, die Beziehungen zwischen dem Controller und den Kontextklassen zu konzipieren, im Vergleich dazu, dass das Repository- oder Arbeitseinheitsmuster überhaupt nicht verwendet wird.
In dieser Tutorialreihe erstellen Sie keine Komponententests. Eine Einführung in TDD mit einer MVC-Anwendung, die das Repositorymuster verwendet, finden Sie unter Exemplarische Vorgehensweise: Verwenden von TDD mit ASP.NET MVC. Weitere Informationen zum Repositorymuster finden Sie in den folgenden Ressourcen:
- Das Repositorymuster auf MSDN.
- Agile Entity Framework 4 Repository-Reihe von Beiträgen im Blog von Julie Lerman.
- Erstellen des Kontos auf einen Blick HTML5/jQuery-Anwendung im Blog von Dan Wahlin.
Hinweis
Es gibt viele Möglichkeiten zum Implementieren von Repository- und Arbeitseinheitsmustern. Sie können Repositoryklassen mit oder ohne eine Arbeitseinheit verwenden. Sie können ein einzelnes Repository für alle Entitätstypen oder eines für jeden Typ implementieren. Wenn Sie eine für jeden Typ implementieren, können Sie separate Klassen, eine generische Basisklasse und abgeleitete Klassen oder eine abstrakte Basisklasse und abgeleitete Klassen verwenden. Sie können Geschäftslogik in Ihr Repository aufnehmen oder auf die Datenzugriffslogik beschränken. Sie können auch eine Abstraktionsebene in Ihre Datenbankkontextklasse erstellen, indem Sie dort IDbSet-Schnittstellen anstelle von DbSet-Typen für Ihre Entitätssätze verwenden. Der in diesem Tutorial gezeigte Ansatz zur Implementierung einer Abstraktionsebene ist eine Option, die Sie berücksichtigen sollten, und nicht eine Empfehlung für alle Szenarien und Umgebungen.
Erstellen der Schülerrepository-Klasse
Erstellen Sie im Ordner DAL eine Klassendatei mit dem Namen IStudentRepository.cs , und ersetzen Sie den vorhandenen Code durch den folgenden 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();
}
}
Dieser Code deklariert einen typischen Satz von CRUD-Methoden, einschließlich zwei Lesemethoden – eine, die alle Student
Entitäten zurückgibt, und eine, die eine einzelne Student
Entität anhand der ID findet.
Erstellen Sie im Ordner DAL eine Klassendatei mit dem Namen StudentRepository.cs . Ersetzen Sie den vorhandenen Code durch den folgenden Code, der die IStudentRepository
-Schnittstelle implementiert:
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);
}
}
}
Der Datenbankkontext wird in einer Klassenvariablen definiert, und der Konstruktor erwartet, dass das aufrufende Objekt eine instance des Kontexts übergibt:
private SchoolContext context;
public StudentRepository(SchoolContext context)
{
this.context = context;
}
Sie könnten einen neuen Kontext im Repository instanziieren, aber wenn Sie dann mehrere Repositorys in einem Controller verwenden, würde jedes einen separaten Kontext erhalten. Später verwenden Sie mehrere Repositorys im Course
Controller, und Sie werden sehen, wie eine Arbeitseinheit sicherstellen kann, dass alle Repositorys denselben Kontext verwenden.
Das Repository implementiert IDisposable und entsorgt den Datenbankkontext, wie Sie zuvor auf dem Controller gesehen haben, und seine CRUD-Methoden führen Aufrufe an den Datenbankkontext auf die gleiche Weise wie zuvor aus.
Ändern des Studentencontrollers in "Verwenden des Repositorys"
Ersetzen Sie in StudentController.cs den Code, der sich derzeit in der Klasse befindet, durch den folgenden Code. Die Änderungen werden hervorgehoben.
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);
}
}
}
Der Controller deklariert jetzt eine Klassenvariable für ein Objekt, das die IStudentRepository
Schnittstelle anstelle der Kontextklasse implementiert:
private IStudentRepository studentRepository;
Der Standardkonstruktor (parameterlos) erstellt einen neuen Kontext instance, und ein optionaler Konstruktor ermöglicht es dem Aufrufer, einen Kontext instance zu übergeben.
public StudentController()
{
this.studentRepository = new StudentRepository(new SchoolContext());
}
public StudentController(IStudentRepository studentRepository)
{
this.studentRepository = studentRepository;
}
(Wenn Sie abhängigkeitsinjektion oder DI verwenden, benötigen Sie den Standardkonstruktor nicht, da die DI-Software sicherstellen würde, dass immer das richtige Repositoryobjekt bereitgestellt wird.)
In den CRUD-Methoden wird das Repository jetzt anstelle des Kontexts aufgerufen:
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();
Und die Dispose
-Methode verwird jetzt das Repository anstelle des Kontexts:
studentRepository.Dispose();
Führen Sie die Website aus, und klicken Sie auf die Registerkarte Schüler .
Die Seite sieht genauso aus und funktioniert wie vor der Änderung des Codes für die Verwendung des Repositorys, und die anderen Student-Seiten funktionieren ebenfalls genauso. Es gibt jedoch einen wichtigen Unterschied in der Art und Weise, wie die Index
-Methode des Controllers Filterung und Sortierung durchführt. Die ursprüngliche Version dieser Methode enthielt den folgenden 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()));
}
Die aktualisierte Index
Methode enthält den folgenden 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()));
}
Nur der hervorgehobene Code wurde geändert.
In der ursprünglichen Version des Codes students
wird als IQueryable
-Objekt eingegeben. Die Abfrage wird erst an die Datenbank gesendet, wenn sie mithilfe einer Methode wie ToList
in eine Sammlung konvertiert wird. Dies erfolgt erst, wenn die Indexansicht auf das Studentenmodell zugreift. Die Where
Methode im ursprünglichen Code oben wird zu einer WHERE
Klausel in der SQL-Abfrage, die an die Datenbank gesendet wird. Dies bedeutet wiederum, dass nur die ausgewählten Entitäten von der Datenbank zurückgegeben werden. Als Ergebnis der Änderung context.Students
in ist die students
Variable nach dieser Anweisung jedoch eine IEnumerable
Auflistung, die alle Kursteilnehmer in studentRepository.GetStudents()
der Datenbank enthält. Das Endergebnis der Anwendung der Where
-Methode ist dasselbe, aber jetzt erfolgt die Arbeit im Arbeitsspeicher auf dem Webserver und nicht von der Datenbank. Bei Abfragen, die große Datenmengen zurückgeben, kann dies ineffizient sein.
Tipp
IQueryable im Vergleich zu IEnumerable
Nachdem Sie das Repository wie hier gezeigt implementiert haben, gibt die an SQL Server gesendete Abfrage alle Zeilen Student zurück, da sie ihre Suchkriterien nicht enthält:
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'
Diese Abfrage gibt alle Schülerdaten zurück, da das Repository die Abfrage ausgeführt hat, ohne von den Suchkriterien zu wissen. Die Sortierung, das Anwenden von Suchkriterien und das Auswählen einer Teilmenge der Daten für das Paging (in diesem Fall nur 3 Zeilen) erfolgt später im Arbeitsspeicher, wenn die ToPagedList
-Methode für die IEnumerable
Auflistung aufgerufen wird.
In der vorherigen Version des Codes (vor der Implementierung des Repositorys) wird die Abfrage erst an die Datenbank gesendet, nachdem Sie die Suchkriterien angewendet haben, wenn ToPagedList
für das IQueryable
Objekt aufgerufen wird.
Wenn ToPagedList für ein IQueryable
Objekt aufgerufen wird, gibt die an SQL Server gesendete Abfrage die Suchzeichenfolge an, sodass nur Zeilen zurückgegeben werden, die die Suchkriterien erfüllen, und es muss keine Filterung im Arbeitsspeicher durchgeführt werden.
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'
(Im folgenden Tutorial wird erläutert, wie An SQL Server gesendete Abfragen untersucht werden.)
Im folgenden Abschnitt wird gezeigt, wie Repositorymethoden implementiert werden, mit denen Sie angeben können, dass diese Arbeit von der Datenbank ausgeführt werden soll.
Sie haben nun eine Abstraktionsebene zwischen dem Controller und dem Entity Framework-Datenbankkontext erstellt. Wenn Sie automatisierte Komponententests mit dieser Anwendung durchführen würden, könnten Sie eine alternative Repositoryklasse in einem Komponententestprojekt erstellen, das implementiert IStudentRepository
. Anstatt den Kontext zum Lesen und Schreiben von Daten aufzurufen, könnte diese Pseudorepositoryklasse Speicherauflistungen bearbeiten, um Controllerfunktionen zu testen.
Implementieren eines generischen Repositorys und einer Arbeitseinheit
Das Erstellen einer Repositoryklasse für jeden Entitätstyp kann zu einer Menge redundantem Code und zu partiellen Updates führen. Angenommen, Sie müssen zwei verschiedene Entitätstypen im Rahmen derselben Transaktion aktualisieren. Wenn jeweils ein separater Datenbankkontext instance verwendet wird, kann einer erfolgreich sein, und der andere kann fehlschlagen. Eine Möglichkeit, redundanten Code zu minimieren, ist die Verwendung eines generischen Repositorys. Eine Möglichkeit, sicherzustellen, dass alle Repositorys denselben Datenbankkontext verwenden (und somit alle Updates koordinieren), besteht darin, eine Arbeitsklasse zu verwenden.
In diesem Abschnitt des Tutorials erstellen Sie eine GenericRepository
Klasse und eine UnitOfWork
Klasse und verwenden sie im Course
Controller, um sowohl auf die Department
Entitätssätze als auch auf die Course
Entitätensätze zuzugreifen. Wie bereits erwähnt, erstellen Sie keine Schnittstellen für diese Klassen, um diesen Teil des Tutorials einfach zu halten. Wenn Sie sie jedoch verwenden würden, um TDD zu vereinfachen, würden Sie sie in der Regel mit Schnittstellen auf die gleiche Weise implementieren, wie Sie es im Student
Repository getan haben.
Erstellen eines generischen Repositorys
Erstellen Sie im Ordner DALGenericRepository.cs , und ersetzen Sie den vorhandenen Code durch den folgenden 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;
}
}
}
Klassenvariablen werden für den Datenbankkontext und für den Entitätssatz deklariert, für den das Repository instanziiert wird:
internal SchoolContext context;
internal DbSet dbSet;
Der Konstruktor akzeptiert einen Datenbankkontext instance und initialisiert die Entitätssatzvariable:
public GenericRepository(SchoolContext context)
{
this.context = context;
this.dbSet = context.Set<TEntity>();
}
Die Get
-Methode verwendet Lambdaausdrücke, damit der aufrufende Code eine Filterbedingung und eine Spalte angeben kann, nach der die Ergebnisse sortiert werden, und mit einem Zeichenfolgenparameter kann der Aufrufer eine durch Trennzeichen getrennte Liste von Navigationseigenschaften für das ausführende Laden bereitstellen:
public virtual IEnumerable<TEntity> Get(
Expression<Func<TEntity, bool>> filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
string includeProperties = "")
Der Code Expression<Func<TEntity, bool>> filter
bedeutet, dass der Aufrufer einen Lambdaausdruck basierend auf dem TEntity
Typ bereitstellt, und dieser Ausdruck gibt einen booleschen Wert zurück. Wenn das Repository beispielsweise für den Entitätstyp Student
instanziiert wird, kann der Code in der aufrufenden Methode " für den filter
Parameter angebenstudent => student.LastName == "Smith
.
Der Code Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy
bedeutet auch, dass der Aufrufer einen Lambdaausdruck bereitstellt. In diesem Fall ist die Eingabe des Ausdrucks jedoch ein IQueryable
Objekt für den TEntity
Typ. Der Ausdruck gibt eine geordnete Version dieses IQueryable
Objekts zurück. Wenn das Repository beispielsweise für den Entitätstyp Student
instanziiert wird, kann der Code in der aufrufenden Methode für den orderBy
Parameter angebenq => q.OrderBy(s => s.LastName)
.
Der Code in der Get
-Methode erstellt ein IQueryable
-Objekt und wendet dann den Filterausdruck an, wenn es einen gibt:
IQueryable<TEntity> query = dbSet;
if (filter != null)
{
query = query.Where(filter);
}
Als Nächstes wendet es die Ausdrücke zum Laden von Eifer an, nachdem die durch Trennzeichen getrennte Liste analysiert wurde:
foreach (var includeProperty in includeProperties.Split
(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty);
}
Schließlich wendet er den orderBy
Ausdruck an, wenn es einen gibt, und gibt die Ergebnisse zurück. Andernfalls werden die Ergebnisse der nicht sortierten Abfrage zurückgegeben:
if (orderBy != null)
{
return orderBy(query).ToList();
}
else
{
return query.ToList();
}
Wenn Sie die Get
-Methode aufrufen, können Sie die von der IEnumerable
-Methode zurückgegebene Auflistung filtern und sortieren, anstatt Parameter für diese Funktionen bereitzustellen. Die Sortier- und Filterarbeiten würden dann aber im Arbeitsspeicher auf dem Webserver erledigt. Mit diesen Parametern stellen Sie sicher, dass die Arbeit von der Datenbank und nicht vom Webserver ausgeführt wird. Alternativ können Sie abgeleitete Klassen für bestimmte Entitätstypen erstellen und spezialisierte Get
Methoden wie GetStudentsInNameOrder
oder GetStudentsByName
hinzufügen. In einer komplexen Anwendung kann dies jedoch zu einer großen Anzahl solcher abgeleiteten Klassen und spezialisierten Methoden führen, die mehr Aufwand für die Verwaltung darstellen können.
Der Code in den GetByID
Methoden , Insert
und Update
ähnelt dem, was Sie im nicht generischen Repository gesehen haben. (Sie stellen keinen Parameter für das Laden von Eifer in der GetByID
Signatur bereit, da Sie mit der Find
-Methode kein eifriges Laden durchführen können.)
Für die Delete
-Methode werden zwei Überladungen bereitgestellt:
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);
}
Mit einer dieser Optionen können Sie nur die ID der zu löschenden Entität übergeben, und eine nimmt eine Entität instance. Wie Sie im Tutorial Umgang mit Parallelität gesehen haben, benötigen Sie für die Parallelitätsbehandlung eine Delete
Methode, die eine Entität instance übernimmt, die den ursprünglichen Wert einer Nachverfolgungseigenschaft enthält.
Dieses generische Repository behandelt typische CRUD-Anforderungen. Wenn ein bestimmter Entitätstyp besondere Anforderungen hat, z. B. komplexere Filterung oder Reihenfolge, können Sie eine abgeleitete Klasse erstellen, die über zusätzliche Methoden für diesen Typ verfügt.
Erstellen der Arbeitseinheit
Die Einheit der Arbeitsklasse dient einem Zweck: Um sicherzustellen, dass sie bei Verwendung mehrerer Repositorys einen einzelnen Datenbankkontext verwenden. Auf diese Weise können Sie nach Abschluss einer Arbeitseinheit die Methode für diese SaveChanges
instance des Kontexts aufrufen und sicher sein, dass alle zugehörigen Änderungen koordiniert werden. Alles, was die Klasse benötigt, ist eine Save
Methode und eine Eigenschaft für jedes Repository. Jede Repositoryeigenschaft gibt ein Repository instance zurück, das mit demselben Datenbankkontext instance instanziiert wurde wie die anderen Repositoryinstanzen.
Erstellen Sie im Ordner DAL eine Klassendatei mit dem Namen UnitOfWork.cs , und ersetzen Sie den Vorlagencode durch den folgenden 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);
}
}
}
Der Code erstellt Klassenvariablen für den Datenbankkontext und jedes Repository. Für die context
Variable wird ein neuer Kontext instanziiert:
private SchoolContext context = new SchoolContext();
private GenericRepository<Department> departmentRepository;
private GenericRepository<Course> courseRepository;
Jede Repositoryeigenschaft überprüft, ob das Repository bereits vorhanden ist. Andernfalls instanziiert es das Repository und übergibt es im Kontext instance. Daher haben alle Repositorys den gleichen Kontext instance.
public GenericRepository<Department> DepartmentRepository
{
get
{
if (this.departmentRepository == null)
{
this.departmentRepository = new GenericRepository<Department>(context);
}
return departmentRepository;
}
}
Die Save
-Methode ruft SaveChanges
den Datenbankkontext auf.
Wie jede Klasse, die einen Datenbankkontext in einer Klassenvariable instanziiert, implementiert IDisposable
und entfernt die UnitOfWork
Klasse den Kontext.
Ändern des Kurscontrollers für die Verwendung der UnitOfWork-Klasse und -Repositorys
Ersetzen Sie den Code, den Sie derzeit in CourseController.cs haben, durch den folgenden 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);
}
}
}
Dieser Code fügt eine Klassenvariable für die UnitOfWork
Klasse hinzu. (Wenn Sie hier Schnittstellen verwenden, würden Sie die Variable hier nicht initialisieren. Stattdessen würden Sie wie für das Student
Repository ein Muster von zwei Konstruktoren implementieren.)
private UnitOfWork unitOfWork = new UnitOfWork();
Im Rest der Klasse werden alle Verweise auf den Datenbankkontext durch Verweise auf das entsprechende Repository ersetzt, wobei Eigenschaften für den Zugriff auf das Repository verwendet UnitOfWork
werden. Die Dispose
-Methode veräußert den 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();
Führen Sie die Website aus, und klicken Sie auf die Registerkarte Kurse .
Die Seite sieht genauso aus und funktioniert wie vor den Änderungen, und auch die anderen Kursseiten funktionieren genauso.
Zusammenfassung
Sie haben jetzt sowohl das Repository als auch das Arbeitseinheitsmuster implementiert. Sie haben Lambdaausdrücke als Methodenparameter im generischen Repository verwendet. Weitere Informationen zur Verwendung dieser Ausdrücke mit einem IQueryable
-Objekt finden Sie unter IQueryable(T) Interface (System.Linq) in der MSDN Library. Im nächsten Tutorial erfahren Sie, wie Sie mit einigen erweiterten Szenarien umgehen.
Links zu anderen Entity Framework-Ressourcen finden Sie in der Inhaltsübersicht ASP.NET Datenzugriff.