Implementace vzorců úložiště a jednotek práce v aplikaci ASP.NET MVC (9 ze 10)
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í.
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í:
- Model úložiště na webu MSDN.
- Agile Entity Framework 4 Repository series příspěvků na blogu Julie Lerman.
- Vytvoření aplikace Account at a Glance HTML5/jQuery na blogu Dana Wahlina.
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 .
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 ToList
je , 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í:
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.
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
, Insert
a 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 .
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.