Partager via


Utilisation d’Entity Framework 4.0 et du contrôle ObjectDataSource, partie 2 : ajout d’une couche de logique métier et de tests unitaires

par Tom Dykstra

Cette série de tutoriels s’appuie sur l’application web Contoso University créée par le Prise en main avec la série de didacticiels Entity Framework 4.0. Si vous n’avez pas terminé les didacticiels précédents, comme point de départ de ce didacticiel, vous pouvez télécharger l’application que vous auriez créée. Vous pouvez également télécharger l’application créée par la série complète de tutoriels. Si vous avez des questions sur les didacticiels, vous pouvez les publier sur le forum ASP.NET Entity Framework.

Dans le tutoriel précédent, vous avez créé une application web multiniveau à l’aide d’Entity Framework et du ObjectDataSource contrôle . Ce tutoriel montre comment ajouter une logique métier tout en conservant la couche de logique métier (BLL) et la couche d’accès aux données (DAL) séparées, et il montre comment créer des tests unitaires automatisés pour la BLL.

Dans ce tutoriel, vous allez effectuer les tâches suivantes :

  • Créez une interface de dépôt qui déclare les méthodes d’accès aux données dont vous avez besoin.
  • Implémentez l’interface du dépôt dans la classe du dépôt.
  • Créez une classe de logique métier qui appelle la classe du référentiel pour effectuer des fonctions d’accès aux données.
  • Connectez le ObjectDataSource contrôle à la classe de logique métier plutôt qu’à la classe de dépôt.
  • Créez un projet de test unitaire et une classe de référentiel qui utilise des collections en mémoire pour son magasin de données.
  • Créez un test unitaire pour la logique métier que vous souhaitez ajouter à la classe de logique métier, puis exécutez le test et voyez qu’il échoue.
  • Implémentez la logique métier dans la classe de logique métier, puis réexécutez le test unitaire et voyez-le réussir.

Vous allez travailler avec les pages Departments.aspx et DepartmentsAdd.aspx que vous avez créées dans le tutoriel précédent.

Création d’une interface de dépôt

Vous allez commencer par créer l’interface du dépôt.

Image08

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

using System;
using System.Collections.Generic;

namespace ContosoUniversity.DAL
{
    public interface ISchoolRepository : IDisposable
    {
        IEnumerable<Department> GetDepartments();
        void InsertDepartment(Department department);
        void DeleteDepartment(Department department);
        void UpdateDepartment(Department department, Department origDepartment);
        IEnumerable<InstructorName> GetInstructorNames();
    }
}

L’interface définit une méthode pour chacune des méthodes CRUD (create, read, update, delete) que vous avez créées dans la classe du dépôt.

Dans la SchoolRepository classe de SchoolRepository.cs, indiquez que cette classe implémente l’interface ISchoolRepository :

public class SchoolRepository : IDisposable, ISchoolRepository

Création d’une classe Business-Logic

Ensuite, vous allez créer la classe de logique métier. Vous effectuez cette opération afin d’ajouter une logique métier qui sera exécutée par le ObjectDataSource contrôle, bien que vous ne le fassiez pas encore. Pour le moment, la nouvelle classe de logique métier effectue uniquement les mêmes opérations CRUD que le dépôt.

Image09

Créez un dossier et nommez-le BLL. (Dans une application réelle, la couche de logique métier est généralement implémentée en tant que bibliothèque de classes (un projet distinct), mais pour simplifier ce didacticiel, les classes BLL sont conservées dans un dossier de projet.)

Dans le dossier BLL , créez un fichier de classe, nommez-le SchoolBL.cs et remplacez le code existant par le code suivant :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using ContosoUniversity.DAL;

namespace ContosoUniversity.BLL
{
    public class SchoolBL : IDisposable
    {
        private ISchoolRepository schoolRepository;

        public SchoolBL()
        {
            this.schoolRepository = new SchoolRepository();
        }

        public SchoolBL(ISchoolRepository schoolRepository)
        {
            this.schoolRepository = schoolRepository;
        }

        public IEnumerable<Department> GetDepartments()
        {
            return schoolRepository.GetDepartments();
        }

        public void InsertDepartment(Department department)
        {
            try
            {
                schoolRepository.InsertDepartment(department);
            }
            catch (Exception ex)
            {
                //Include catch blocks for specific exceptions first,
                //and handle or log the error as appropriate in each.
                //Include a generic catch block like this one last.
                throw ex;
            }
        }

        public void DeleteDepartment(Department department)
        {
            try
            {
                schoolRepository.DeleteDepartment(department);
            }
            catch (Exception ex)
            {
                //Include catch blocks for specific exceptions first,
                //and handle or log the error as appropriate in each.
                //Include a generic catch block like this one last.
                throw ex;
            }
        }

        public void UpdateDepartment(Department department, Department origDepartment)
        {
            try
            {
                schoolRepository.UpdateDepartment(department, origDepartment);
            }
            catch (Exception ex)
            {
                //Include catch blocks for specific exceptions first,
                //and handle or log the error as appropriate in each.
                //Include a generic catch block like this one last.
                throw ex;
            }

        }

        public IEnumerable<InstructorName> GetInstructorNames()
        {
            return schoolRepository.GetInstructorNames();
        }

        private bool disposedValue = false;

        protected virtual void Dispose(bool disposing)
        {
            if (!this.disposedValue)
            {
                if (disposing)
                {
                    schoolRepository.Dispose();
                }
            }
            this.disposedValue = true;
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

    }
}

Ce code crée les mêmes méthodes CRUD que celles que vous avez vues précédemment dans la classe du dépôt, mais au lieu d’accéder directement aux méthodes Entity Framework, il appelle les méthodes de classe de dépôt.

La variable de classe qui contient une référence à la classe du référentiel est définie comme un type d’interface, et le code qui instancie la classe de dépôt est contenu dans deux constructeurs. Le constructeur sans paramètre sera utilisé par le ObjectDataSource contrôle . Il crée un instance de la SchoolRepository classe que vous avez créée précédemment. L’autre constructeur permet au code qui instancie la classe de logique métier de passer n’importe quel objet qui implémente l’interface du dépôt.

Les méthodes CRUD qui appellent la classe du référentiel et les deux constructeurs permettent d’utiliser la classe de logique métier avec le magasin de données principal de votre choix. La classe de logique métier n’a pas besoin de savoir comment la classe qu’elle appelle conserve les données. (C’est souvent ce qu’on appelle l’ignorance de persistance.) Cela facilite les tests unitaires, car vous pouvez connecter la classe de logique métier à une implémentation de référentiel qui utilise quelque chose d’aussi simple que des collections en mémoire List pour stocker des données.

Notes

Techniquement, les objets d’entité ne sont toujours pas ignorants de la persistance, car ils sont instanciés à partir de classes qui héritent de la classe d’Entity EntityObject Framework. Pour une ignorance complète de la persistance, vous pouvez utiliser d’anciens objets CLR simples, ou POCO, à la place d’objets qui héritent de la EntityObject classe . L’utilisation de POCOs dépasse le cadre de ce didacticiel. Pour plus d’informations, consultez Testabilité et Entity Framework 4.0 sur le site web MSDN.)

Vous pouvez maintenant connecter les ObjectDataSource contrôles à la classe de logique métier plutôt qu’au dépôt et vérifier que tout fonctionne comme avant.

Dans Departments.aspx et DepartmentsAdd.aspx, remplacez chaque occurrence de TypeName="ContosoUniversity.DAL.SchoolRepository" par TypeName="ContosoUniversity.BLL.SchoolBL». (Il y a quatre instances en tout.)

Exécutez les pages Departments.aspx et DepartmentsAdd.aspx pour vérifier qu’elles fonctionnent toujours comme avant.

Image01

Image02

Création d’une implémentation de projet et de dépôt Unit-Test

Ajoutez un nouveau projet à la solution à l’aide du modèle Projet de test et nommez-le ContosoUniversity.Tests.

Dans le projet de test, ajoutez une référence à System.Data.Entity et ajoutez une référence de projet au ContosoUniversity projet.

Vous pouvez maintenant créer la classe de référentiel que vous allez utiliser avec des tests unitaires. Le magasin de données de ce dépôt se trouve dans la classe .

Image12

Dans le projet de test, créez un fichier de classe, nommez-le MockSchoolRepository.cs et remplacez le code existant par le code suivant :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ContosoUniversity.DAL;
using ContosoUniversity.BLL;

namespace ContosoUniversity.Tests
{
    class MockSchoolRepository : ISchoolRepository, IDisposable
    {
        List<Department> departments = new List<Department>();
        List<InstructorName> instructors = new List<InstructorName>();

        public IEnumerable<Department> GetDepartments()
        {
            return departments;
        }

        public void InsertDepartment(Department department)
        {
            departments.Add(department);
        }

        public void DeleteDepartment(Department department)
        {
            departments.Remove(department);
        }

        public void UpdateDepartment(Department department, Department origDepartment)
        {
            departments.Remove(origDepartment);
            departments.Add(department);
        }

        public IEnumerable<InstructorName> GetInstructorNames()
        {
            return instructors;
        }

        public void Dispose()
        {
            
        }
    }
}

Cette classe de dépôt a les mêmes méthodes CRUD que celle qui accède directement à Entity Framework, mais elles fonctionnent avec List des collections en mémoire plutôt qu’avec une base de données. Cela permet à une classe de test de configurer et de valider plus facilement des tests unitaires pour la classe de logique métier.

Création de tests unitaires

Le modèle de projet Test a créé une classe de test unitaire stub pour vous, et votre tâche suivante consiste à modifier cette classe en lui ajoutant des méthodes de test unitaire pour la logique métier que vous souhaitez ajouter à la classe de logique métier.

Image13

À l’université contoso, chaque instructeur individuel ne peut être administrateur que d’un seul service, et vous devez ajouter une logique métier pour appliquer cette règle. Vous allez commencer par ajouter des tests et exécuter les tests pour les voir échouer. Vous allez ensuite ajouter le code et réexécuter les tests, en vous attendant de les voir réussir.

Ouvrez le fichier UnitTest1.cs et ajoutez using des instructions pour les couches de logique métier et d’accès aux données que vous avez créées dans le projet ContosoUniversity :

using ContosoUniversity.BLL;
using ContosoUniversity.DAL;

Remplacez la TestMethod1 méthode par les méthodes suivantes :

private SchoolBL CreateSchoolBL()
{
    var schoolRepository = new MockSchoolRepository();
    var schoolBL = new SchoolBL(schoolRepository);
    schoolBL.InsertDepartment(new Department() { Name = "First Department", DepartmentID = 0, Administrator = 1, Person = new Instructor () { FirstMidName = "Admin", LastName = "One" } });
    schoolBL.InsertDepartment(new Department() { Name = "Second Department", DepartmentID = 1, Administrator = 2, Person = new Instructor() { FirstMidName = "Admin", LastName = "Two" } });
    schoolBL.InsertDepartment(new Department() { Name = "Third Department", DepartmentID = 2, Administrator = 3, Person = new Instructor() { FirstMidName = "Admin", LastName = "Three" } });
    return schoolBL;
}

[TestMethod]
[ExpectedException(typeof(DuplicateAdministratorException))]
public void AdministratorAssignmentRestrictionOnInsert()
{
    var schoolBL = CreateSchoolBL();
    schoolBL.InsertDepartment(new Department() { Name = "Fourth Department", DepartmentID = 3, Administrator = 2, Person = new Instructor() { FirstMidName = "Admin", LastName = "Two" } });
}

[TestMethod]
[ExpectedException(typeof(DuplicateAdministratorException))]
public void AdministratorAssignmentRestrictionOnUpdate()
{
    var schoolBL = CreateSchoolBL();
    var origDepartment = (from d in schoolBL.GetDepartments()
                          where d.Name == "Second Department"
                          select d).First();
    var department = (from d in schoolBL.GetDepartments()
                          where d.Name == "Second Department"
                          select d).First();
    department.Administrator = 1;
    schoolBL.UpdateDepartment(department, origDepartment);
}

La CreateSchoolBL méthode crée un instance de la classe de référentiel que vous avez créée pour le projet de test unitaire, qu’elle transmet ensuite à une nouvelle instance de la classe de logique métier. La méthode utilise ensuite la classe de logique métier pour insérer trois services que vous pouvez utiliser dans les méthodes de test.

Les méthodes de test vérifient que la classe de logique métier lève une exception si quelqu’un tente d’insérer un nouveau service avec le même administrateur qu’un service existant, ou si quelqu’un tente de mettre à jour l’administrateur d’un service en lui affectant l’ID d’une personne qui est déjà l’administrateur d’un autre service.

Comme vous n’avez pas encore créé la classe d’exception, ce code ne sera pas compilé. Pour qu’il soit compilé, cliquez avec le bouton DuplicateAdministratorException droit et sélectionnez Générer, puis Classe.

Capture d’écran montrant l’option Générer sélectionnée dans le sous-menu Classe.

Cela crée une classe dans le projet de test que vous pouvez supprimer une fois que vous avez créé la classe d’exception dans le projet main. et implémenté la logique métier.

Exécutez le projet de test. Comme prévu, les tests échouent.

Image03

Ajout de la logique métier pour effectuer une passe de test

Ensuite, vous allez implémenter la logique métier qui rend impossible la définition en tant qu’administrateur d’un service d’une personne qui est déjà administrateur d’un autre service. Vous allez lever une exception à partir de la couche de logique métier, puis l’intercepter dans la couche de présentation si un utilisateur modifie un service et clique sur Mettre à jour après avoir sélectionné une personne déjà administrateur. (Vous pouvez également supprimer de la liste déroulante les instructeurs qui sont déjà administrateurs avant d’afficher la page, mais l’objectif ici est de travailler avec la couche de logique métier.)

Commencez par créer la classe d’exception que vous allez lever lorsqu’un utilisateur tente de faire d’un instructeur l’administrateur de plusieurs services. Dans le projet main, créez un fichier de classe dans le dossier BLL, nommez-le DuplicateAdministratorException.cs et remplacez le code existant par le code suivant :

using System;

namespace ContosoUniversity.BLL
{
    public class DuplicateAdministratorException : Exception
    {
        public DuplicateAdministratorException(string message)
            : base(message)
        {
        }
    }
}

À présent, supprimez le fichier Temporaire DuplicateAdministratorException.cs que vous avez créé dans le projet de test précédemment afin de pouvoir compiler.

Dans le projet main, ouvrez le fichier SchoolBL.cs et ajoutez la méthode suivante qui contient la logique de validation. (Le code fait référence à une méthode que vous créerez ultérieurement.)

private void ValidateOneAdministratorAssignmentPerInstructor(Department department)
{
    if (department.Administrator != null)
    {
        var duplicateDepartment = schoolRepository.GetDepartmentsByAdministrator(department.Administrator.GetValueOrDefault()).FirstOrDefault();
        if (duplicateDepartment != null && duplicateDepartment.DepartmentID != department.DepartmentID)
        {
            throw new DuplicateAdministratorException(String.Format(
                "Instructor {0} {1} is already administrator of the {2} department.", 
                duplicateDepartment.Person.FirstMidName, 
                duplicateDepartment.Person.LastName, 
                duplicateDepartment.Name));
        }
    }
}

Vous allez appeler cette méthode lorsque vous insérez ou mettez à jour Department des entités afin de case activée si un autre service a déjà le même administrateur.

Le code appelle une méthode pour rechercher dans la base de données une Department entité qui a la même Administrator valeur de propriété que l’entité insérée ou mise à jour. Si l’une d’elles est trouvée, le code lève une exception. Aucune case activée de validation n’est requise si l’entité en cours d’insertion ou de mise à jour n’a aucune Administrator valeur, et aucune exception n’est levée si la méthode est appelée pendant une mise à jour et que l’entité Department trouvée correspond à l’entité Department en cours de mise à jour.

Appelez la nouvelle méthode à partir des Insert méthodes et Update :

public void InsertDepartment(Department department)
{
    ValidateOneAdministratorAssignmentPerInstructor(department);
    try
    ...

public void UpdateDepartment(Department department, Department origDepartment)
{
    ValidateOneAdministratorAssignmentPerInstructor(department);
    try
    ...

Dans ISchoolRepository.cs, ajoutez la déclaration suivante pour la nouvelle méthode d’accès aux données :

IEnumerable<Department> GetDepartmentsByAdministrator(Int32 administrator);

Dans SchoolRepository.cs, ajoutez l’instruction suivante using :

using System.Data.Objects;

Dans SchoolRepository.cs, ajoutez la nouvelle méthode d’accès aux données suivante :

public IEnumerable<Department> GetDepartmentsByAdministrator(Int32 administrator)
{
    return new ObjectQuery<Department>("SELECT VALUE d FROM Departments as d", context, MergeOption.NoTracking).Include("Person").Where(d => d.Administrator == administrator).ToList();
}

Ce code récupère les Department entités qui ont un administrateur spécifié. Un seul service doit être trouvé (le cas échéant). Toutefois, étant donné qu’aucune contrainte n’est intégrée dans la base de données, le type de retour est une collection au cas où plusieurs services sont trouvés.

Par défaut, lorsque le contexte de l’objet récupère des entités de la base de données, il effectue le suivi de celles-ci dans son gestionnaire d’état d’objet. Le MergeOption.NoTracking paramètre spécifie que ce suivi ne sera pas effectué pour cette requête. Cela est nécessaire, car la requête peut retourner l’entité exacte que vous essayez de mettre à jour, et vous ne pourrez alors pas attacher cette entité. Par exemple, si vous modifiez le service Historique dans la page Department.aspx et laissez l’administrateur inchangé, cette requête renvoie le service Historique. Si NoTracking n’est pas défini, le contexte de l’objet a déjà l’entité service Historique dans son gestionnaire d’état d’objet. Ensuite, lorsque vous attachez l’entité du service d’historique qui est recréée à partir de l’état d’affichage, le contexte de l’objet lève une exception indiquant "An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key".

(Au lieu de spécifier MergeOption.NoTracking, vous pouvez créer un contexte d’objet uniquement pour cette requête. Étant donné que le nouveau contexte d’objet aurait son propre gestionnaire d’état d’objet, il n’y aurait aucun conflit lorsque vous appelez la Attach méthode . Le nouveau contexte d’objet partagerait les métadonnées et la connexion à la base de données avec le contexte d’objet d’origine, de sorte que la pénalité de performances de cette autre approche serait minimale. Toutefois, l’approche présentée ici présente l’option NoTracking , qui vous sera utile dans d’autres contextes. L’option NoTracking est décrite plus en détail dans un didacticiel ultérieur de cette série.)

Dans le projet de test, ajoutez la nouvelle méthode d’accès aux données à MockSchoolRepository.cs :

public IEnumerable<Department> GetDepartmentsByAdministrator(Int32 administrator)
{
    return (from d in departments
            where d.Administrator == administrator
            select d);
}

Ce code utilise LINQ pour effectuer la même sélection de données que celle utilisée par le dépôt de ContosoUniversity projet LINQ to Entities.

Réexécutez le projet de test. Cette fois-ci, les tests réussissent.

Image04

Gestion des exceptions ObjectDataSource

Dans le ContosoUniversity projet, exécutez la page Departments.aspx et essayez de remplacer l’administrateur d’un service par une personne qui est déjà administrateur d’un autre service. (N’oubliez pas que vous pouvez uniquement modifier les services que vous avez ajoutés au cours de ce didacticiel, car la base de données est préchargée avec des données non valides.) Vous obtenez la page d’erreur de serveur suivante :

Image05

Vous ne souhaitez pas que les utilisateurs voient ce type de page d’erreur. Vous devez donc ajouter un code de gestion des erreurs. Ouvrez Departments.aspx et spécifiez un gestionnaire pour l’événement OnUpdated de .DepartmentsObjectDataSource La ObjectDataSource balise d’ouverture ressemble maintenant à l’exemple suivant.

<asp:ObjectDataSource ID="DepartmentsObjectDataSource" runat="server" 
        TypeName="ContosoUniversity.BLL.SchoolBL"
        DataObjectTypeName="ContosoUniversity.DAL.Department" 
        SelectMethod="GetDepartments" 
        DeleteMethod="DeleteDepartment" 
        UpdateMethod="UpdateDepartment"
        ConflictDetection="CompareAllValues"
        OldValuesParameterFormatString="orig{0}" 
        OnUpdated="DepartmentsObjectDataSource_Updated" >

Dans Departments.aspx.cs, ajoutez l’instruction suivante using :

using ContosoUniversity.BLL;

Ajoutez le gestionnaire suivant pour l’événement Updated :

protected void DepartmentsObjectDataSource_Updated(object sender, ObjectDataSourceStatusEventArgs e)
{
    if (e.Exception != null)
    {
        if (e.Exception.InnerException is DuplicateAdministratorException)
        {
            var duplicateAdministratorValidator = new CustomValidator();
            duplicateAdministratorValidator.IsValid = false;
            duplicateAdministratorValidator.ErrorMessage = "Update failed: " + e.Exception.InnerException.Message;
            Page.Validators.Add(duplicateAdministratorValidator);
            e.ExceptionHandled = true;
        }
    }
}

Si le ObjectDataSource contrôle intercepte une exception lorsqu’il tente d’effectuer la mise à jour, il transmet l’exception dans l’argument d’événement (e) à ce gestionnaire. Le code dans le gestionnaire vérifie si l’exception est l’exception d’administrateur en double. Si tel est le cas, le code crée un contrôle de validateur qui contient un message d’erreur que le ValidationSummary contrôle doit afficher.

Exécutez la page et essayez de faire de quelqu’un l’administrateur de deux services. Cette fois, le ValidationSummary contrôle affiche un message d’erreur.

Image06

Apportez des modifications similaires à la page DepartmentsAdd.aspx . Dans DepartmentsAdd.aspx, spécifiez un gestionnaire pour l’événement OnInserted de .DepartmentsObjectDataSource Le balisage résultant ressemblera à l’exemple suivant.

<asp:ObjectDataSource ID="DepartmentsObjectDataSource" runat="server" 
        TypeName="ContosoUniversity.BLL.SchoolBL" DataObjectTypeName="ContosoUniversity.DAL.Department" 
        InsertMethod="InsertDepartment"  
        OnInserted="DepartmentsObjectDataSource_Inserted">

Dans DepartmentsAdd.aspx.cs, ajoutez la même using instruction :

using ContosoUniversity.BLL;

Ajoutez le gestionnaire d’événements suivant :

protected void DepartmentsObjectDataSource_Inserted(object sender, ObjectDataSourceStatusEventArgs e)
{
    if (e.Exception != null)
    {
        if (e.Exception.InnerException is DuplicateAdministratorException)
        {
            var duplicateAdministratorValidator = new CustomValidator();
            duplicateAdministratorValidator.IsValid = false;
            duplicateAdministratorValidator.ErrorMessage = "Insert failed: " + e.Exception.InnerException.Message;
            Page.Validators.Add(duplicateAdministratorValidator);
            e.ExceptionHandled = true;
        }
    }
}

Vous pouvez maintenant tester la page DépartementsAdd.aspx.cs pour vérifier qu’elle gère également correctement les tentatives visant à faire d’une personne l’administrateur de plusieurs services.

Ceci termine l’introduction à l’implémentation du modèle de dépôt pour l’utilisation du ObjectDataSource contrôle avec Entity Framework. Pour plus d’informations sur le modèle de dépôt et la testabilité, consultez le livre blanc MSDN testabilité et Entity Framework 4.0.

Dans le tutoriel suivant, vous allez voir comment ajouter des fonctionnalités de tri et de filtrage à l’application.