Implementazione dei modelli di repository e unità di lavoro in un'applicazione MVC ASP.NET (9 di 10)
di Tom Dykstra
L'applicazione Web di esempio Contoso University illustra come creare ASP.NET applicazioni MVC 4 usando Entity Framework 5 Code First e Visual Studio 2012. Per informazioni sulla serie di esercitazioni, vedere la prima esercitazione della serie.
Nota
Se si verifica un problema che non è possibile risolvere, scaricare il capitolo completato e provare a riprodurre il problema. In genere è possibile trovare la soluzione al problema confrontando il codice con il codice completato. Per alcuni errori comuni e come risolverli, vedere Errori e soluzioni alternative.
Nell'esercitazione precedente è stata usata l'ereditarietà per ridurre il Student
codice ridondante nelle classi di entità e Instructor
. In questa esercitazione verranno illustrati alcuni modi per usare il repository e l'unità di modelli di lavoro per le operazioni CRUD. Come nell'esercitazione precedente, in questa operazione si modificherà il modo in cui il codice funziona con le pagine già create invece di creare nuove pagine.
Repository e modelli di lavoro unità di lavoro
Il repository e l'unità di modelli di lavoro sono progettati per creare un livello di astrazione tra il livello di accesso ai dati e il livello della logica di business di un'applicazione. L'implementazione di questi modelli può essere utile per isolare l'applicazione dalle modifiche nell'archivio dati e può semplificare il testing unità automatizzato o lo sviluppo basato su test (TDD).
In questa esercitazione si implementerà una classe di repository per ogni tipo di entità. Per il Student
tipo di entità si creeranno un'interfaccia del repository e una classe di repository. Quando si crea un'istanza del repository nel controller, si userà l'interfaccia in modo che il controller accetti un riferimento a qualsiasi oggetto che implementa l'interfaccia del repository. Quando il controller viene eseguito in un server Web, riceve un repository che funziona con Entity Framework. Quando il controller viene eseguito in una classe di unit test, riceve un repository che funziona con i dati archiviati in modo che sia possibile modificare facilmente per i test, ad esempio una raccolta in memoria.
Più avanti nell'esercitazione si useranno più repository e un'unità di classe di lavoro per i Course
tipi di entità e Department
nel Course
controller. L'unità della classe di lavoro coordina il lavoro di più repository creando una singola classe di contesto di database condivisa da tutti. Se si vuole poter eseguire unit test automatizzati, è necessario creare e usare interfacce per queste classi nello stesso modo in cui si è fatto per il Student
repository. Tuttavia, per semplificare l'esercitazione, si creeranno e si useranno queste classi senza interfacce.
La figura seguente mostra un modo per concettualizzare le relazioni tra le classi controller e di contesto rispetto all'uso del repository o dell'unità di lavoro.
Non verranno creati unit test in questa serie di esercitazioni. Per un'introduzione a TDD con un'applicazione MVC che usa il modello di repository, vedere Procedura dettagliata: Uso di TDD con ASP.NET MVC. Per altre informazioni sul modello di repository, vedere le risorse seguenti:
- Modello di repository su MSDN.
- Agile Entity Framework 4 Serie di post nel blog di Julie Lerman.
- Creazione dell'account a colpo d'occhio html5/jQuery application (Creazione dell'account a colpo d'occhio HTML5/jQuery) nel blog di Dan Wahlin.
Nota
Esistono molti modi per implementare il repository e l'unità di modelli di lavoro. È possibile usare classi di repository con o senza un'unità di classe di lavoro. È possibile implementare un singolo repository per tutti i tipi di entità o uno per ogni tipo. Se si implementa uno per ogni tipo, è possibile usare classi separate, una classe di base generica e classi derivate o una classe base astratta e classi derivate. È possibile includere la logica di business nel repository o limitarla alla logica di accesso ai dati. È anche possibile compilare un livello di astrazione nella classe del contesto di database usando interfacce IDbSet invece dei tipi DbSet per i set di entità. L'approccio all'implementazione di un livello di astrazione illustrato in questa esercitazione è un'opzione da considerare, non una raccomandazione per tutti gli scenari e gli ambienti.
Creazione della classe del repository degli studenti
Nella cartella DAL creare un file di classe denominato IStudentRepository.cs e sostituire il codice esistente con il codice seguente:
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();
}
}
Questo codice dichiara un set tipico di metodi CRUD, inclusi due metodi di lettura, uno che restituisce tutte le Student
entità e uno che trova una singola Student
entità in base all'ID.
Nella cartella DAL creare un file di classe denominato StudentRepository.cs . Sostituire il codice esistente con il codice seguente, che implementa l'interfaccia IStudentRepository
:
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);
}
}
}
Il contesto del database è definito in una variabile di classe e il costruttore prevede che l'oggetto chiamante passi un'istanza del contesto:
private SchoolContext context;
public StudentRepository(SchoolContext context)
{
this.context = context;
}
È possibile creare un'istanza di un nuovo contesto nel repository, ma se sono stati usati più repository in un controller, ognuno di essi finirà con un contesto separato. Più avanti si useranno più repository nel Course
controller e si vedrà in che modo un'unità di classe di lavoro può garantire che tutti i repository usino lo stesso contesto.
Il repository implementa IDisposable ed elimina il contesto del database come illustrato in precedenza nel controller e i relativi metodi CRUD eseguono chiamate al contesto del database nello stesso modo illustrato in precedenza.
Modificare il controller degli studenti in modo che usi il repository
In StudentController.cs sostituire il codice attualmente presente nella classe con il codice seguente. Le modifiche sono evidenziate.
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);
}
}
}
Il controller dichiara ora una variabile di classe per un oggetto che implementa l'interfaccia IStudentRepository
anziché la classe di contesto:
private IStudentRepository studentRepository;
Il costruttore predefinito (senza parametri) crea una nuova istanza del contesto e un costruttore facoltativo consente al chiamante di passare un'istanza di contesto.
public StudentController()
{
this.studentRepository = new StudentRepository(new SchoolContext());
}
public StudentController(IStudentRepository studentRepository)
{
this.studentRepository = studentRepository;
}
Se si usa l'inserimento delle dipendenze o l'inserimento delle dipendenze, non è necessario il costruttore predefinito perché il software di inserimento delle dipendenze garantisce che venga sempre fornito l'oggetto repository corretto.
Nei metodi CRUD il repository viene ora chiamato invece del contesto:
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
Il metodo elimina ora il repository anziché il contesto:
studentRepository.Dispose();
Eseguire il sito e fare clic sulla scheda Studenti .
La pagina ha lo stesso aspetto e funziona come in precedenza prima di modificare il codice per usare il repository e le altre pagine Student funzionano allo stesso modo. Tuttavia, esiste una differenza importante nel modo in cui il Index
metodo del controller filtra e ordina. La versione originale di questo metodo conteneva il codice seguente:
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()));
}
Il metodo aggiornato Index
contiene il codice seguente:
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()));
}
Solo il codice evidenziato è stato modificato.
Nella versione originale del codice viene students
digitato come IQueryable
oggetto . La query non viene inviata al database finché non viene convertita in una raccolta usando un metodo, ad esempio ToList
, che non si verifica finché la visualizzazione Index non accede al modello student. Il Where
metodo nel codice originale precedente diventa una WHERE
clausola nella query SQL inviata al database. Ciò significa che solo le entità selezionate vengono restituite dal database. Tuttavia, in seguito alla modifica context.Students
di studentRepository.GetStudents()
, la students
variabile dopo questa istruzione è una IEnumerable
raccolta che include tutti gli studenti nel database. Il risultato finale dell'applicazione del Where
metodo è lo stesso, ma ora il lavoro viene eseguito in memoria nel server Web e non dal database. Per le query che restituiscono grandi volumi di dati, ciò può risultare inefficiente.
Suggerimento
IQueryable e IEnumerable
Dopo aver implementato il repository come illustrato di seguito, anche se si immette qualcosa nella casella Di ricerca la query inviata a SQL Server restituisce tutte le righe Student perché non include i criteri di ricerca:
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'
Questa query restituisce tutti i dati degli studenti perché il repository ha eseguito la query senza conoscere i criteri di ricerca. Il processo di ordinamento, l'applicazione dei criteri di ricerca e la selezione di un subset dei dati per il paging (che mostra solo 3 righe in questo caso) viene eseguita in memoria in un secondo momento quando il ToPagedList
metodo viene chiamato nella IEnumerable
raccolta.
Nella versione precedente del codice (prima dell'implementazione del repository), la query non viene inviata al database fino a quando non si applicano i criteri di ricerca, quando ToPagedList
viene chiamata sull'oggetto IQueryable
.
Quando ToPagedList viene chiamato su un IQueryable
oggetto, la query inviata a SQL Server specifica la stringa di ricerca e, di conseguenza, vengono restituite solo le righe che soddisfano i criteri di ricerca e non è necessario eseguire filtri in memoria.
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'
L'esercitazione seguente illustra come esaminare le query inviate a SQL Server.
La sezione seguente illustra come implementare i metodi del repository che consentono di specificare che questa operazione deve essere eseguita dal database.
È stato creato un livello di astrazione tra il controller e il contesto del database Entity Framework. Se si intende eseguire unit test automatizzati con questa applicazione, è possibile creare una classe di repository alternativa in un progetto di unit test che implementa IStudentRepository
. Anziché chiamare il contesto per leggere e scrivere dati, questa classe di repository fittizia potrebbe modificare le raccolte in memoria per testare le funzioni del controller.
Implementare un repository generico e un'unità di lavoro
La creazione di una classe di repository per ogni tipo di entità potrebbe comportare un numero elevato di codice ridondante e potrebbe comportare aggiornamenti parziali. Si supponga, ad esempio, di dover aggiornare due tipi di entità diversi come parte della stessa transazione. Se ognuna usa un'istanza del contesto di database separata, è possibile che una abbia esito positivo e che l'altra abbia esito negativo. Un modo per ridurre al minimo il codice ridondante consiste nell'usare un repository generico e un modo per garantire che tutti i repository usino lo stesso contesto di database (e quindi coordinare tutti gli aggiornamenti) consiste nell'usare un'unità di classe di lavoro.
In questa sezione dell'esercitazione si creeranno una GenericRepository
classe e una UnitOfWork
classe e le si userà nel Course
controller per accedere Department
ai set di entità e Course
. Come illustrato in precedenza, per mantenere semplice questa parte dell'esercitazione, non si creano interfacce per queste classi. Tuttavia, se si intende usarli per facilitare il TDD, in genere si implementano con interfacce nello stesso modo in cui è stato eseguito il Student
repository.
Creare un repository generico
Nella cartella DAL creare GenericRepository.cs e sostituire il codice esistente con il codice seguente:
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;
}
}
}
Le variabili di classe vengono dichiarate per il contesto del database e per il set di entità per cui viene creata un'istanza del repository:
internal SchoolContext context;
internal DbSet dbSet;
Il costruttore accetta un'istanza del contesto di database e inizializza la variabile del set di entità:
public GenericRepository(SchoolContext context)
{
this.context = context;
this.dbSet = context.Set<TEntity>();
}
Il Get
metodo usa espressioni lambda per consentire al codice chiamante di specificare una condizione di filtro e una colonna per ordinare i risultati e un parametro stringa consente al chiamante di fornire un elenco delimitato da virgole di proprietà di navigazione per il caricamento eager:
public virtual IEnumerable<TEntity> Get(
Expression<Func<TEntity, bool>> filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
string includeProperties = "")
Il codice Expression<Func<TEntity, bool>> filter
indica che il chiamante fornirà un'espressione lambda in base al TEntity
tipo e questa espressione restituirà un valore booleano. Ad esempio, se viene creata un'istanza del repository per il Student
tipo di entità, il codice nel metodo chiamante potrebbe specificare student => student.LastName == "Smith
" per il filter
parametro .
Il codice Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy
indica anche che il chiamante fornirà un'espressione lambda. In questo caso, tuttavia, l'input per l'espressione è un IQueryable
oggetto per il TEntity
tipo . L'espressione restituirà una versione ordinata di tale IQueryable
oggetto. Ad esempio, se viene creata un'istanza del repository per il Student
tipo di entità, il codice nel metodo chiamante potrebbe specificare q => q.OrderBy(s => s.LastName)
per il orderBy
parametro .
Il codice nel Get
metodo crea un IQueryable
oggetto e quindi applica l'espressione di filtro, se presente:
IQueryable<TEntity> query = dbSet;
if (filter != null)
{
query = query.Where(filter);
}
Applica quindi le espressioni eager-loading dopo l'analisi dell'elenco delimitato da virgole:
foreach (var includeProperty in includeProperties.Split
(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty);
}
Infine, applica l'espressione orderBy
se ne esiste una e restituisce i risultati; in caso contrario, restituisce i risultati della query non ordinata:
if (orderBy != null)
{
return orderBy(query).ToList();
}
else
{
return query.ToList();
}
Quando si chiama il Get
metodo , è possibile filtrare e ordinare la IEnumerable
raccolta restituita dal metodo anziché fornire parametri per queste funzioni. Tuttavia, il lavoro di ordinamento e filtro verrebbe eseguito in memoria nel server Web. Usando questi parametri, assicurarsi che il lavoro venga eseguito dal database anziché dal server Web. Un'alternativa consiste nel creare classi derivate per tipi di entità specifici e aggiungere metodi specializzati Get
, ad esempio GetStudentsInNameOrder
o GetStudentsByName
. Tuttavia, in un'applicazione complessa, questo può comportare un numero elevato di classi derivate e metodi specializzati, che potrebbero essere più lavoro da gestire.
Il codice nei GetByID
metodi , Insert
e Update
è simile a quello visualizzato nel repository non generico. Non si specifica un parametro di caricamento eager nella GetByID
firma, perché non è possibile eseguire il caricamento eager con il Find
metodo .
Per il Delete
metodo vengono forniti due overload:
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);
}
Uno di questi consente di passare solo l'ID dell'entità da eliminare e uno accetta un'istanza di entità. Come illustrato nell'esercitazione Sulla gestione della concorrenza , per la gestione della concorrenza è necessario un metodo che accetta un'istanza Delete
di entità che include il valore originale di una proprietà di rilevamento.
Questo repository generico gestirà i requisiti CRUD tipici. Quando un tipo di entità specifico presenta requisiti speciali, ad esempio filtri o ordinamento più complessi, è possibile creare una classe derivata con metodi aggiuntivi per tale tipo.
Creazione dell'unità di lavoro
L'unità di classe di lavoro serve a uno scopo: per assicurarsi che quando si usano più repository, condividono un singolo contesto di database. In questo modo, quando un'unità di lavoro è completa, è possibile chiamare il SaveChanges
metodo su tale istanza del contesto e assicurarsi che tutte le modifiche correlate saranno coordinate. Tutto ciò che la classe necessita è un Save
metodo e una proprietà per ogni repository. Ogni proprietà del repository restituisce un'istanza del repository di cui è stata creata un'istanza usando la stessa istanza del contesto di database delle altre istanze del repository.
Nella cartella DAL creare un file di classe denominato UnitOfWork.cs e sostituire il codice del modello con il codice seguente:
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);
}
}
}
Il codice crea variabili di classe per il contesto del database e ogni repository. Per la context
variabile viene creata un'istanza di un nuovo contesto:
private SchoolContext context = new SchoolContext();
private GenericRepository<Department> departmentRepository;
private GenericRepository<Course> courseRepository;
Ogni proprietà del repository controlla se il repository esiste già. In caso contrario, crea un'istanza del repository, passando l'istanza del contesto. Di conseguenza, tutti i repository condividono la stessa istanza di contesto.
public GenericRepository<Department> DepartmentRepository
{
get
{
if (this.departmentRepository == null)
{
this.departmentRepository = new GenericRepository<Department>(context);
}
return departmentRepository;
}
}
Il Save
metodo chiama SaveChanges
nel contesto del database.
Come qualsiasi classe che crea un'istanza di un contesto di database in una variabile di classe, la UnitOfWork
classe implementa IDisposable
ed elimina il contesto.
Modifica del controller del corso per l'uso della classe e dei repository UnitOfWork
Sostituire il codice attualmente disponibile in CourseController.cs con il codice seguente:
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);
}
}
}
Questo codice aggiunge una variabile di classe per la UnitOfWork
classe . Se si usassero le interfacce qui, non si inizializzerebbe la variabile qui. In alternativa, si implementerebbe un modello di due costruttori esattamente come è stato fatto per il Student
repository.
private UnitOfWork unitOfWork = new UnitOfWork();
Nella parte restante della classe, tutti i riferimenti al contesto del database vengono sostituiti da riferimenti al repository appropriato, usando UnitOfWork
le proprietà per accedere al repository. Il Dispose
metodo elimina l'istanza UnitOfWork
.
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();
Eseguire il sito e fare clic sulla scheda Corsi .
La pagina ha lo stesso aspetto e funziona come prima delle modifiche e anche le altre pagine course funzionano allo stesso modo.
Riepilogo
Sono stati implementati sia il repository che l'unità di modelli di lavoro. Sono state usate espressioni lambda come parametri del metodo nel repository generico. Per altre informazioni su come usare queste espressioni con un IQueryable
oggetto , vedere IQueryable(T) Interface (System.Linq) in MSDN Library. Nell'esercitazione successiva si apprenderà come gestire alcuni scenari avanzati.
I collegamenti ad altre risorse di Entity Framework sono disponibili nella mappa del contenuto ASP.NET accesso ai dati.