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.

Repository_pattern_diagram

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:

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 .

Students_Index_page

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 ToListin 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:

Screenshot des Codes, der das neue Studentenrepository implementiert und hervorgehoben zeigt.

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.

Screenshot, der den Code des Studentencontrollers zeigt. Eine Suchzeichenfolgenzeile mit Code und die Codezeile In ausgelagerte Liste sind hervorgehoben.

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 GetStudentsByNamehinzufü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 GetByIDMethoden , Insertund 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 .

Courses_Index_page

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.