Implémentation des modèles de référentiel et d’unité de travail dans une application MVC ASP.NET (9 sur 10)

par Tom Dykstra

L’exemple d’application web Contoso University montre comment créer ASP.NET applications MVC 4 à l’aide d’Entity Framework 5 Code First et de Visual Studio 2012. Pour obtenir des informations sur la série de didacticiels, consultez le premier didacticiel de la série.

Notes

Si vous rencontrez un problème que vous ne pouvez pas résoudre, téléchargez le chapitre terminé et essayez de le reproduire. Vous pouvez généralement trouver la solution au problème en comparant votre code au code terminé. Pour connaître certaines erreurs courantes et la façon de les résoudre, consultez Erreurs et solutions de contournement.

Dans le tutoriel précédent, vous avez utilisé l’héritage pour réduire le code redondant dans les classes d’entité Student et Instructor . Dans ce tutoriel, vous verrez quelques façons d’utiliser le référentiel et les modèles d’unité de travail pour les opérations CRUD. Comme dans le tutoriel précédent, dans celui-ci, vous allez modifier la façon dont votre code fonctionne avec les pages que vous avez déjà créées plutôt que de créer de nouvelles pages.

Modèles de dépôt et d’unité de travail

Le référentiel et l’unité de travail sont destinés à créer une couche d’abstraction entre la couche d’accès aux données et la couche logique métier d’une application. L’implémentation de ces modèles peut favoriser l’isolation de votre application face à des modifications dans le magasin de données et peut faciliter le test unitaire automatisé ou le développement piloté par les tests (TDD).

Dans ce tutoriel, vous allez implémenter une classe de référentiel pour chaque type d’entité. Pour le Student type d’entité, vous allez créer une interface de dépôt et une classe de référentiel. Lorsque vous instanciez le dépôt dans votre contrôleur, vous utilisez l’interface afin que le contrôleur accepte une référence à n’importe quel objet qui implémente l’interface du référentiel. Lorsque le contrôleur s’exécute sous un serveur web, il reçoit un dépôt qui fonctionne avec Entity Framework. Lorsque le contrôleur s’exécute sous une classe de test unitaire, il reçoit un dépôt qui fonctionne avec des données stockées d’une manière que vous pouvez facilement manipuler à des fins de test, comme une collection en mémoire.

Plus loin dans le tutoriel, vous utiliserez plusieurs référentiels et une unité de classe de travail pour les types d’entités Course et Department dans le Course contrôleur. L’unité de classe de travail coordonne le travail de plusieurs référentiels en créant une seule classe de contexte de base de données partagée par tous. Si vous souhaitez pouvoir effectuer des tests unitaires automatisés, vous devez créer et utiliser des interfaces pour ces classes de la même façon que pour le Student dépôt. Toutefois, pour que le tutoriel reste simple, vous allez créer et utiliser ces classes sans interfaces.

L’illustration suivante montre une façon de conceptualiser les relations entre le contrôleur et les classes de contexte par rapport à ne pas utiliser du tout le modèle de dépôt ou d’unité de travail.

Repository_pattern_diagram

Vous ne créerez pas de tests unitaires dans cette série de tutoriels. Pour une présentation de TDD avec une application MVC qui utilise le modèle de dépôt, consultez Procédure pas à pas : utilisation de TDD avec ASP.NET MVC. Pour plus d’informations sur le modèle de dépôt, consultez les ressources suivantes :

Notes

Il existe de nombreuses façons d’implémenter le référentiel et les modèles d’unité de travail. Vous pouvez utiliser des classes de référentiel avec ou sans unité de classe de travail. Vous pouvez implémenter un référentiel unique pour tous les types d’entités, ou un pour chaque type. Si vous en implémentez une pour chaque type, vous pouvez utiliser des classes distinctes, une classe de base générique et des classes dérivées, ou une classe de base abstraite et des classes dérivées. Vous pouvez inclure la logique métier dans votre dépôt ou la restreindre à la logique d’accès aux données. Vous pouvez également créer une couche d’abstraction dans votre classe de contexte de base de données en utilisant des interfaces IDbSet à la place des types DbSet pour vos jeux d’entités. L’approche d’implémentation d’une couche d’abstraction présentée dans ce tutoriel est une option à considérer, et non une recommandation pour tous les scénarios et environnements.

Création de la classe de référentiel d’étudiants

Dans le dossier DAL , créez un fichier de classe nommé IStudentRepository.cs et remplacez le code existant par le code suivant :

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

Ce code déclare un ensemble classique de méthodes CRUD, y compris deux méthodes de lecture , une qui retourne toutes les Student entités et une qui recherche une entité unique Student par ID.

Dans le dossier DAL , créez un fichier de classe nommé Fichier StudentRepository.cs . Remplacez le code existant par le code suivant, qui implémente l’interface 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);
        }
    }
}

Le contexte de base de données est défini dans une variable de classe, et le constructeur s’attend à ce que l’objet appelant passe un instance du contexte :

private SchoolContext context;

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

Vous pouvez instancier un nouveau contexte dans le dépôt, mais si vous utilisiez plusieurs référentiels dans un contrôleur, chacun se retrouverait avec un contexte distinct. Plus tard, vous utiliserez plusieurs dépôts dans le Course contrôleur et vous verrez comment une unité de classe de travail peut garantir que tous les dépôts utilisent le même contexte.

Le référentiel implémente IDisposable et supprime le contexte de base de données comme vous l’avez vu précédemment dans le contrôleur, et ses méthodes CRUD effectuent des appels au contexte de base de données de la même manière que vous l’avez vu précédemment.

Modifier le contrôleur d’étudiant pour utiliser le référentiel

Dans StudentController.cs, remplacez le code actuellement dans la classe par le code suivant. Les modifications sont mises en surbrillance.

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

Le contrôleur déclare maintenant une variable de classe pour un objet qui implémente l’interface IStudentRepository au lieu de la classe de contexte :

private IStudentRepository studentRepository;

Le constructeur par défaut (sans paramètre) crée un nouveau contexte instance, et un constructeur facultatif permet à l’appelant de passer dans un contexte instance.

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

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

(Si vous utilisiez l’injection de dépendances, ou l’injection de dépendances, vous n’auriez pas besoin du constructeur par défaut, car le logiciel DI garantit que l’objet de dépôt correct est toujours fourni.)

Dans les méthodes CRUD, le dépôt est maintenant appelé au lieu du contexte :

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

Et la Dispose méthode supprime maintenant le dépôt au lieu du contexte :

studentRepository.Dispose();

Exécutez le site, puis cliquez sur l’onglet Étudiants .

Students_Index_page

La page regarde et fonctionne de la même façon qu’avant que vous ne modifiiez le code pour utiliser le référentiel, et les autres pages Étudiant fonctionnent également de la même façon. Toutefois, il existe une différence importante dans la façon dont la Index méthode du contrôleur effectue le filtrage et l’ordre. La version d’origine de cette méthode contenait le code suivant :

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

La méthode mise à jour Index contient le code suivant :

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

Seul le code en surbrillance a changé.

Dans la version d’origine du code, students est tapé en tant qu’objet IQueryable . La requête n’est pas envoyée à la base de données tant qu’elle n’est pas convertie en collection à l’aide d’une méthode telle que , ce qui ne se produit pas tant que ToListla vue Index n’accède pas au modèle étudiant. La Where méthode dans le code d’origine ci-dessus devient une WHERE clause dans la requête SQL envoyée à la base de données. Cela signifie à son tour que seules les entités sélectionnées sont retournées par la base de données. Toutefois, en raison de la modification en context.StudentsstudentRepository.GetStudents(), la students variable après cette instruction est une IEnumerable collection qui inclut tous les étudiants de la base de données. Le résultat final de l’application de la Where méthode est le même, mais maintenant le travail est effectué en mémoire sur le serveur web et non par la base de données. Pour les requêtes qui retournent de grands volumes de données, cela peut être inefficace.

Conseil

IQueryable et IEnumerable

Après avoir implémenté le dépôt comme indiqué ici, même si vous entrez quelque chose dans la zone De recherche, la requête envoyée à SQL Server retourne toutes les lignes Student, car elle n’inclut pas vos critères de recherche :

Capture d’écran du code montrant le nouveau dépôt d’étudiants implémenté et mis en surbrillance.

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'

Cette requête retourne toutes les données de l’étudiant, car le référentiel a exécuté la requête sans connaître les critères de recherche. Le processus de tri, d’application de critères de recherche et de sélection d’un sous-ensemble des données pour la pagination (n’affichant que 3 lignes dans ce cas) est effectué en mémoire ultérieurement lorsque la ToPagedList méthode est appelée sur la IEnumerable collection.

Dans la version précédente du code (avant d’implémenter le dépôt), la requête n’est envoyée à la base de données qu’après avoir appliqué les critères de recherche, quand ToPagedList est appelée sur l’objet IQueryable .

Capture d’écran montrant le code Student Controller. Une ligne de chaîne de recherche du code et la ligne To Pagd List du code sont mises en surbrillance.

Lorsque ToPagedList est appelé sur un IQueryable objet, la requête envoyée à SQL Server spécifie la chaîne de recherche et, par conséquent, seules les lignes qui répondent aux critères de recherche sont retournées et aucun filtrage n’est nécessaire en mémoire.

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'

(Le tutoriel suivant explique comment examiner les requêtes envoyées à SQL Server.)

La section suivante montre comment implémenter des méthodes de dépôt qui vous permettent de spécifier que ce travail doit être effectué par la base de données.

Vous avez maintenant créé une couche d’abstraction entre le contrôleur et le contexte de base de données Entity Framework. Si vous deviez effectuer des tests unitaires automatisés IStudentRepository avec cette application, vous pouvez créer une autre classe de référentiel dans un projet de test unitaire qui implémente . Au lieu d’appeler le contexte pour lire et écrire des données, cette classe de référentiel fictif peut manipuler des collections en mémoire afin de tester les fonctions du contrôleur.

Implémenter un dépôt générique et une unité de classe de travail

La création d’une classe de référentiel pour chaque type d’entité peut entraîner beaucoup de code redondant et entraîner des mises à jour partielles. Par exemple, supposons que vous devez mettre à jour deux types d’entités différents dans le cadre de la même transaction. Si chacun utilise un contexte de base de données distinct instance, l’un peut réussir et l’autre peut échouer. Une façon de réduire le code redondant consiste à utiliser un dépôt générique, et une façon de s’assurer que tous les dépôts utilisent le même contexte de base de données (et donc de coordonner toutes les mises à jour) consiste à utiliser une unité de classe de travail.

Dans cette section du tutoriel, vous allez créer une classe et une GenericRepositoryUnitOfWork classe, et les utiliser dans le Course contrôleur pour accéder aux jeux d’entités Department et Course . Comme expliqué précédemment, pour simplifier cette partie du tutoriel, vous ne créez pas d’interfaces pour ces classes. Mais si vous deviez les utiliser pour faciliter la tdd, vous les implémentez généralement avec des interfaces de la même façon que vous avez fait le Student dépôt.

Créer un dépôt générique

Dans le dossier DAL , créez GenericRepository.cs et remplacez le code existant par le code suivant :

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

Les variables de classe sont déclarées pour le contexte de base de données et pour le jeu d’entités pour lequel le dépôt est instancié :

internal SchoolContext context;
internal DbSet dbSet;

Le constructeur accepte un contexte de base de données instance et initialise la variable de jeu d’entités :

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

La Get méthode utilise des expressions lambda pour permettre au code appelant de spécifier une condition de filtre et une colonne pour classer les résultats par, et un paramètre de chaîne permet à l’appelant de fournir une liste délimitée par des virgules de propriétés de navigation pour un chargement pressant :

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

Le code Expression<Func<TEntity, bool>> filter signifie que l’appelant fournira une expression lambda basée sur le TEntity type, et cette expression retournera une valeur booléenne. Par exemple, si le dépôt est instancié pour le Student type d’entité, le code dans la méthode appelante peut spécifier student => student.LastName == "Smith« pour le filter paramètre .

Le code Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy signifie également que l’appelant fournira une expression lambda. Mais dans ce cas, l’entrée de l’expression est un IQueryable objet pour le TEntity type . L’expression retourne une version ordonnée de cet IQueryable objet. Par exemple, si le dépôt est instancié pour le Student type d’entité, le code de la méthode appelante peut spécifier q => q.OrderBy(s => s.LastName) pour le orderBy paramètre .

Le code de la Get méthode crée un IQueryable objet, puis applique l’expression de filtre s’il en existe un :

IQueryable<TEntity> query = dbSet;

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

Ensuite, il applique les expressions à chargement hâtif après l’analyse de la liste délimitée par des virgules :

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

Enfin, il applique l’expression s’il orderBy en existe une et retourne les résultats ; sinon, il retourne les résultats de la requête non triée :

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

Lorsque vous appelez la Get méthode , vous pouvez effectuer un filtrage et un tri sur la IEnumerable collection retournée par la méthode au lieu de fournir des paramètres pour ces fonctions. Mais le travail de tri et de filtrage est ensuite effectué en mémoire sur le serveur web. En utilisant ces paramètres, vous vous assurez que le travail est effectué par la base de données plutôt que par le serveur web. Une alternative consiste à créer des classes dérivées pour des types d’entités spécifiques et à ajouter des méthodes spécialisées Get , telles que GetStudentsInNameOrder ou GetStudentsByName. Toutefois, dans une application complexe, cela peut entraîner un grand nombre de classes dérivées et de méthodes spécialisées, qui pourraient être plus difficiles à gérer.

Le code dans les GetByIDméthodes , Insertet Update est similaire à ce que vous avez vu dans le dépôt non générique. (Vous ne fournissez pas de paramètre de chargement hâtif dans la GetByID signature, car vous ne pouvez pas effectuer de chargement avec la Find méthode .)

Deux surcharges sont fournies pour la Delete méthode :

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

L’une d’elles vous permet de passer uniquement l’ID de l’entité à supprimer, et l’une d’elles prend une entité instance. Comme vous l’avez vu dans le didacticiel Gestion de la concurrence, pour la gestion de l’accès concurrentiel, vous avez besoin d’une Delete méthode qui prend une entité instance qui inclut la valeur d’origine d’une propriété de suivi.

Ce dépôt générique gère les exigences CRUD classiques. Lorsqu’un type d’entité particulier a des exigences particulières, telles que le filtrage ou l’ordre plus complexes, vous pouvez créer une classe dérivée qui a des méthodes supplémentaires pour ce type.

Création de la classe d’unité de travail

L’unité de classe de travail a un seul objectif : s’assurer que lorsque vous utilisez plusieurs dépôts, ils partagent un seul contexte de base de données. De cette façon, lorsqu’une unité de travail est terminée, vous pouvez appeler la SaveChanges méthode sur cette instance du contexte et être assuré que toutes les modifications associées seront coordonnées. La classe n’a besoin que d’une Save méthode et d’une propriété pour chaque dépôt. Chaque propriété de dépôt retourne un dépôt instance qui a été instancié à l’aide du même contexte de base de données instance que les autres instances de dépôt.

Dans le dossier DAL , créez un fichier de classe nommé UnitOfWork.cs et remplacez le code du modèle par le code suivant :

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

Le code crée des variables de classe pour le contexte de base de données et chaque dépôt. Pour la context variable, un nouveau contexte est instancié :

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

Chaque propriété de dépôt vérifie si le dépôt existe déjà. Si ce n’est pas le cas, il instancie le dépôt, en passant dans le contexte instance. Par conséquent, tous les dépôts partagent le même contexte instance.

public GenericRepository<Department> DepartmentRepository
{
    get
    {

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

La Save méthode appelle SaveChanges sur le contexte de base de données.

Comme toute classe qui instancie un contexte de base de données dans une variable de classe, la UnitOfWork classe implémente IDisposable et supprime le contexte.

Modification du contrôleur de cours pour utiliser la classe et les référentiels UnitOfWork

Remplacez le code que vous avez actuellement dans CourseController.cs par le code suivant :

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

Ce code ajoute une variable de classe pour la UnitOfWork classe . (Si vous utilisiez des interfaces ici, vous n’initialiseriez pas la variable ici; à la place, vous implémenteriez un modèle de deux constructeurs comme vous l’avez fait pour le Student dépôt.)

private UnitOfWork unitOfWork = new UnitOfWork();

Dans le reste de la classe, toutes les références au contexte de base de données sont remplacées par des références au référentiel approprié, en utilisant UnitOfWork des propriétés pour accéder au dépôt. La Dispose méthode supprime le UnitOfWork instance.

var courses = unitOfWork.CourseRepository.Get(includeProperties: "Department");
// ...
Course course = unitOfWork.CourseRepository.GetByID(id);
// ...
unitOfWork.CourseRepository.Insert(course);
unitOfWork.Save();
// ...
Course course = unitOfWork.CourseRepository.GetByID(id);
// ...
unitOfWork.CourseRepository.Update(course);
unitOfWork.Save();
// ...
var departmentsQuery = unitOfWork.DepartmentRepository.Get(
    orderBy: q => q.OrderBy(d => d.Name));
// ...
Course course = unitOfWork.CourseRepository.GetByID(id);
// ...
unitOfWork.CourseRepository.Delete(id);
unitOfWork.Save();
// ...
unitOfWork.Dispose();

Exécutez le site et cliquez sur l’onglet Cours .

Courses_Index_page

La page se présente et fonctionne de la même façon qu’avant vos modifications, et les autres pages de cours fonctionnent également de la même façon.

Résumé

Vous avez maintenant implémenté le référentiel et l’unité de travail modèles. Vous avez utilisé des expressions lambda comme paramètres de méthode dans le référentiel générique. Pour plus d’informations sur l’utilisation de ces expressions avec un IQueryable objet, consultez IQueryable(T) Interface (System.Linq) dans MSDN Library. Dans le tutoriel suivant, vous allez apprendre à gérer certains scénarios avancés.

Vous trouverez des liens vers d’autres ressources Entity Framework dans le ASP.NET Data Access Content Map.