Implementace vzorců úložiště a jednotek práce v aplikaci ASP.NET MVC (9 ze 10)

Tom Dykstra

Ukázková webová aplikace Contoso University ukazuje, jak vytvářet aplikace ASP.NET MVC 4 pomocí entity Framework 5 Code First a sady Visual Studio 2012. Informace o sérii kurzů najdete v prvním kurzu v této sérii.

Poznámka

Pokud narazíte na problém, který nemůžete vyřešit, stáhněte si dokončenou kapitolu a zkuste problém reprodukovat. Obecně můžete najít řešení problému porovnáním kódu s dokončeným kódem. Informace o některých běžných chybách a jejich řešení najdete v tématu Chyby a alternativní řešení.

V předchozím kurzu jste použili dědičnost k omezení redundantního kódu v Student třídách entit a Instructor . V tomto kurzu se seznámíte s některými způsoby použití úložiště a pracovních vzorů jednotek pro operace CRUD. Stejně jako v předchozím kurzu i v tomto kurzu změníte způsob, jakým váš kód funguje se stránkami, které jste už vytvořili, místo toho, abyste vytvářeli nové stránky.

Vzory úložiště a jednotky práce

Úložiště a pracovní vzory jednotek slouží k vytvoření abstraktní vrstvy mezi vrstvou přístupu k datům a vrstvou obchodní logiky aplikace. Implementace těchto vzorů může pomoct izolovat vaši aplikaci od změn v úložišti dat a může usnadnit automatizované testování částí nebo vývoj řízený testy (TDD).

V tomto kurzu implementujete třídu úložiště pro každý typ entity. Student Pro typ entity vytvoříte rozhraní úložiště a třídu úložiště. Při vytváření instance úložiště v kontroleru použijete rozhraní, aby kontroler přijal odkaz na jakýkoli objekt, který implementuje rozhraní úložiště. Když kontroler běží pod webovým serverem, obdrží úložiště, které funguje s Rozhraním Entity Framework. Když kontroler běží v rámci třídy testování jednotek, obdrží úložiště, které pracuje s daty uloženými způsobem, se kterým můžete snadno manipulovat pro testování, jako je například kolekce v paměti.

Později v tomto kurzu použijete více úložišť a třídu práce pro Course typy entit a Department v Course kontroleru. Jednotka pracovní třídy koordinuje práci více úložišť vytvořením jedné třídy kontextu databáze sdílené všemi z nich. Pokud byste chtěli být schopni provádět automatizované testování částí, vytvořili byste rozhraní pro tyto třídy a používali je stejným způsobem jako pro Student úložiště. Aby byl tento kurz jednoduchý, vytvoříte a použijete tyto třídy bez rozhraní.

Následující obrázek znázorňuje jeden ze způsobů, jak konceptualizovat vztahy mezi kontrolerem a třídami kontextu ve srovnání s tím, že se úložiště nebo pracovní jednotka vůbec nepoužívají.

Repository_pattern_diagram

V této sérii kurzů nebudete vytvářet testy jednotek. Úvod do TDD s aplikací MVC, která používá vzor úložiště, najdete v tématu Návod: Použití TDD s ASP.NET MVC. Další informace o vzoru úložiště najdete v následujících zdrojích informací:

Poznámka

Existuje mnoho způsobů, jak implementovat vzory úložiště a jednotek práce. Třídy úložiště můžete používat s jednotkou pracovní třídy nebo bez této třídy. Můžete implementovat jedno úložiště pro všechny typy entit nebo jedno pro každý typ. Pokud implementujete jeden pro každý typ, můžete použít samostatné třídy, obecnou základní třídu a odvozené třídy nebo abstraktní základní třídu a odvozené třídy. Obchodní logiku můžete zahrnout do úložiště nebo ji omezit na logiku přístupu k datům. Vrstvu abstrakce můžete také vytvořit do třídy kontextu databáze pomocí rozhraní IDbSet místo typů DbSet pro sady entit. Přístup k implementaci vrstvy abstrakce uvedený v tomto kurzu je jednou z možností, kterou byste měli zvážit, nikoli doporučení pro všechny scénáře a prostředí.

Vytvoření třídy úložiště studenta

Ve složce DAL vytvořte soubor třídy s názvem IStudentRepository.cs a nahraďte existující kód následujícím kódem:

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

Tento kód deklaruje typickou sadu metod CRUD, včetně dvou metod čtení – jedné, která vrací všechny Student entity, a jedné, která najde jednu Student entitu podle ID.

Ve složce DAL vytvořte soubor třídy s názvem StudentRepository.cs . Nahraďte existující kód následujícím kódem, který implementuje IStudentRepository rozhraní:

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

Kontext databáze je definován v proměnné třídy a konstruktor očekává, že volající objekt předá instanci kontextu:

private SchoolContext context;

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

V úložišti byste mohli vytvořit instanci nového kontextu, ale pokud byste pak použili více úložišť v jednom kontroleru, každé z nich by skončilo se samostatným kontextem. Později v kontroleru použijete více úložišť Course a uvidíte, jak může jednotka pracovní třídy zajistit, aby všechna úložiště používala stejný kontext.

Úložiště implementuje IDisposable a odstraňuje kontext databáze, jak jste viděli dříve v kontroleru, a jeho metody CRUD provádějí volání kontextu databáze stejným způsobem, jaký jste viděli dříve.

Změňte studentský kontroler tak, aby používal úložiště.

V souboru StudentController.cs nahraďte kód, který je aktuálně ve třídě, následujícím kódem. Změny jsou zvýrazněné.

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 teď deklaruje proměnnou třídy pro objekt, který implementuje IStudentRepository rozhraní místo třídy kontextu:

private IStudentRepository studentRepository;

Výchozí (bez parametrů) konstruktor vytvoří novou instanci kontextu a volitelný konstruktor umožňuje volajícímu předat instanci kontextu.

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

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

(Pokud byste používali injektáž závislostí, nebo DI, výchozí konstruktor byste nepotřebovali, protože software pro injektáže závislostí zajistí, že se vždy poskytne správný objekt úložiště.)

V metodách CRUD se teď místo kontextu volá úložiště:

var students = from s in studentRepository.GetStudents()
               select s;
Student student = studentRepository.GetStudentByID(id);
studentRepository.InsertStudent(student);
studentRepository.Save();
studentRepository.UpdateStudent(student);
studentRepository.Save();
studentRepository.DeleteStudent(id);
studentRepository.Save();

Dispose A metoda teď místo kontextu odstraní úložiště:

studentRepository.Dispose();

Spusťte web a klikněte na kartu Studenti .

Students_Index_page

Stránka vypadá a funguje stejně jako před změnou kódu tak, aby používal úložiště, a stejně fungují i ostatní stránky Studentů. Je ale důležitý rozdíl ve způsobu, jakým Index metoda kontroleru filtruje a seřadí. Původní verze této metody obsahovala následující kód:

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

Aktualizovaná Index metoda obsahuje následující kód:

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

Změnil se jenom zvýrazněný kód.

V původní verzi kódu students je zadán jako IQueryable objekt . Dotaz se do databáze neodesílají, dokud se nepřevedí na kolekci pomocí metody, jako ToListje , k čemuž nedojde, dokud zobrazení indexu nepřistupuje k modelu studenta. Metoda Where v původním kódu výše se stane klauzulí WHERE v dotazu SQL, který se odešle do databáze. To znamená, že databáze vrátí jenom vybrané entity. V důsledku změny context.Students na studentRepository.GetStudents()students však proměnná za tímto příkazem představuje kolekciIEnumerable, která zahrnuje všechny studenty v databázi. Konečný výsledek použití Where metody je stejný, ale teď se práce provádí v paměti na webovém serveru a ne v databázi. U dotazů, které vracejí velké objemy dat, to může být neefektivní.

Tip

IQueryable vs. IEnumerable

Po implementaci úložiště, jak je znázorněno tady, vrátí dotaz odeslaný do SQL Server i když něco zadáte do vyhledávacího pole, všechny řádky Studenta, protože neobsahuje vaše kritéria hledání:

Snímek obrazovky s kódem, který zobrazuje implementované a zvýrazněné nové úložiště studentů

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'

Tento dotaz vrátí všechna data studentů, protože úložiště dotaz spustilo, aniž by vědělo o kritériích vyhledávání. Proces řazení, použití kritérií hledání a výběru podmnožině dat pro stránkování (v tomto případě pouze 3 řádky) se provede v paměti později, když ToPagedList je metoda volána v kolekci IEnumerable .

V předchozí verzi kódu (před implementací úložiště) se dotaz do databáze neodesílají, dokud nepoužádáte kritéria vyhledávání při ToPagedList zavolání na IQueryable objekt.

Snímek obrazovky s kódem Student Controller Řádek kódu vyhledávacího řetězce a řádek kódu Do stránkovaného seznamu jsou zvýrazněné.

Při je volána ToPagedList u objektuIQueryable, dotaz odeslaný do SQL Server určuje hledaný řetězec a v důsledku toho jsou vráceny pouze řádky, které splňují kritéria hledání, a není nutné v paměti provádět žádné filtrování.

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'

(Následující kurz vysvětluje, jak prozkoumat dotazy odeslané do SQL Server.)

Následující část ukazuje, jak implementovat metody úložiště, které umožňují určit, že tuto práci má provést databáze.

Právě jste vytvořili vrstvu abstrakce mezi kontrolerem a kontextem databáze Entity Framework. Pokud byste chtěli provádět automatizované testování částí pomocí této aplikace, mohli byste vytvořit alternativní třídu úložiště v projektu testování jednotek, který implementuje IStudentRepository. Místo volání kontextu ke čtení a zápisu dat může tato třída úložiště napodobení pracovat s kolekcemi v paměti za účelem testování funkcí kontroleru.

Implementace obecného úložiště a třídy pracovní jednotky

Vytvoření třídy úložiště pro každý typ entity může vést k velkému množství redundantního kódu a k částečným aktualizacím. Předpokládejme například, že v rámci stejné transakce musíte aktualizovat dva různé typy entit. Pokud každá používá samostatnou instanci kontextu databáze, jedna může být úspěšná a druhá může selhat. Jedním ze způsobů, jak minimalizovat redundantní kód, je použít obecné úložiště. Jedním ze způsobů, jak zajistit, aby všechna úložiště používala stejný kontext databáze (a tedy koordinovat všechny aktualizace), je použít jednotku pracovní třídy.

V této části kurzu vytvoříte GenericRepository třídu a třídu a UnitOfWork použijete je v Course kontroleru pro přístup k sadám Course entit i Department . Jak bylo vysvětleno dříve, aby byla tato část kurzu jednoduchá, nevytáčíte rozhraní pro tyto třídy. Pokud byste je ale chtěli použít k usnadnění TDD, obvykle byste je implementovali s rozhraními stejným způsobem jako úložiště Student .

Vytvoření obecného úložiště

Ve složce DAL vytvořte Soubor GenericRepository.cs a nahraďte stávající kód následujícím kódem:

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

Proměnné třídy jsou deklarovány pro kontext databáze a pro sadu entit, pro kterou je vytvořena instance úložiště:

internal SchoolContext context;
internal DbSet dbSet;

Konstruktor přijme instanci kontextu databáze a inicializuje proměnnou sady entit:

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

Metoda Get pomocí výrazů lambda umožňuje volajícímu kódu zadat podmínku filtru a sloupec pro řazení výsledků podle a řetězcový parametr umožňuje volajícímu zadat seznam vlastností navigace oddělených čárkami pro dychtivé načítání:

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

Kód Expression<Func<TEntity, bool>> filter znamená, že volající poskytne výraz lambda založený na TEntity typu a tento výraz vrátí logickou hodnotu. Pokud je například instance úložiště vytvořena pro Student typ entity, kód ve volající metodě může jako filter parametr zadat student => student.LastName == "Smith" .

Kód Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy také znamená, že volající poskytne výraz lambda. Ale v tomto případě je vstup do výrazu objektem IQueryable pro typ TEntity . Výraz vrátí seřazenou verzi tohoto IQueryable objektu. Pokud je například vytvořena instance úložiště pro Student typ entity, kód ve volající metodě může zadat q => q.OrderBy(s => s.LastName) parametr orderBy .

Kód v Get metodě vytvoří IQueryable objekt a pak použije výraz filter, pokud existuje:

IQueryable<TEntity> query = dbSet;

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

Dále použije výrazy s dychtivým načítáním po parsování seznamu odděleného čárkami:

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

Nakonec výraz použije orderBy , pokud existuje, a vrátí výsledky. V opačném případě vrátí výsledky z neuspořádaného dotazu:

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

Při volání Get metody můžete filtrovat a řadit IEnumerable kolekci vrácenou metodou místo zadávání parametrů pro tyto funkce. Ale řazení a filtrování by se pak provádělo v paměti na webovém serveru. Použitím těchto parametrů zajistíte, že práci provádí databáze, nikoli webový server. Alternativou je vytvoření odvozených tříd pro konkrétní typy entit a přidání specializovaných Get metod, jako GetStudentsInNameOrder je nebo GetStudentsByName. Ve složité aplikaci to však může mít za následek velký počet takových odvozených tříd a specializovaných metod, což by mohlo být více práce je třeba udržovat.

Kód v metodách GetByID, Inserta Update je podobný tomu, co jste viděli v negenerovém úložišti. (V podpisu nezadáte parametr dychtivého GetByID načítání, protože s metodou nemůžete načítat nedočkavé načítání Find .)

Pro metodu Delete jsou k dispozici dvě přetížení:

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

Jedna z těchto možností umožňuje předat pouze ID entity, která se má odstranit, a druhá převezme instanci entity. Jak jste viděli v kurzu Zpracování souběžnosti , pro zpracování souběžnosti potřebujete metodu Delete , která přebírá instanci entity, která obsahuje původní hodnotu vlastnosti sledování.

Toto obecné úložiště bude zpracovávat typické požadavky CRUD. Pokud má konkrétní typ entity zvláštní požadavky, například složitější filtrování nebo řazení, můžete vytvořit odvozenou třídu, která má pro tento typ další metody.

Vytvoření třídy jednotky práce

Jednotka pracovní třídy má jeden účel: zajistit, aby při použití více úložišť sdílely jeden kontext databáze. Díky tomu můžete po dokončení jednotky práce volat metodu SaveChanges pro danou instanci kontextu a mít jistotu, že všechny související změny budou koordinovány. Vše, co třída potřebuje, Save je metoda a vlastnost pro každé úložiště. Každá vlastnost úložiště vrátí instanci úložiště, která byla vytvořena pomocí stejné instance kontextu databáze jako ostatní instance úložiště.

Ve složce DAL vytvořte soubor třídy s názvem UnitOfWork.cs a nahraďte kód šablony následujícím kódem:

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

Kód vytvoří proměnné třídy pro kontext databáze a každé úložiště. Pro proměnnou se context vytvoří nová instance kontextu:

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

Každá vlastnost úložiště kontroluje, jestli úložiště již existuje. Pokud ne, vytvoří instanci úložiště a předá instanci kontextu. V důsledku toho všechna úložiště sdílejí stejnou instanci kontextu.

public GenericRepository<Department> DepartmentRepository
{
    get
    {

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

Metoda Save volá SaveChanges kontext databáze.

Stejně jako každá třída, která vytvoří instanci kontextu databáze v proměnné třídy, UnitOfWork třída implementuje IDisposable a odstraňuje kontext.

Změna kontroleru kurzu tak, aby používal třídu a úložiště UnitOfWork

Nahraďte kód, který aktuálně máte v souboru CourseController.cs , následujícím kódem:

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

Tento kód přidá proměnnou třídy pro UnitOfWork třídu . (Pokud byste zde používali rozhraní, neinicializovali byste proměnnou. Místo toho byste implementovali vzor dvou konstruktorů stejně jako pro Student úložiště.)

private UnitOfWork unitOfWork = new UnitOfWork();

Ve zbývající části třídy jsou všechny odkazy na kontext databáze nahrazeny odkazy na příslušné úložiště pomocí UnitOfWork vlastností pro přístup k úložišti. Metoda Dispose odstraní UnitOfWork instanci .

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

Spusťte web a klikněte na kartu Kurzy .

Courses_Index_page

Stránka vypadá a funguje stejně jako před vašimi změnami a stejně fungují i ostatní stránky kurzu.

Souhrn

Teď jste implementovali vzory úložiště i jednotek práce. Výrazy lambda jste použili jako parametry metody v obecném úložišti. Další informace o použití těchto výrazů s objektem IQueryable naleznete v tématu Rozhraní IQueryable(T) (System.Linq) v knihovně MSDN. V dalším kurzu se dozvíte, jak si poradit s některými pokročilými scénáři.

Odkazy na další prostředky Entity Framework najdete v mapě obsahu ASP.NET Data Access.