Condividi tramite


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.

Repository_pattern_diagram

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:

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 .

Students_Index_page

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:

Screenshot del codice che mostra il nuovo repository degli studenti implementato ed evidenziato.

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 .

Screenshot che mostra il codice Student Controller. Vengono evidenziate una riga di codice della stringa di ricerca e la riga Elenco a paging del codice.

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 GetByIDmetodi , Inserte 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 .

Courses_Index_page

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.