Uso di Entity Framework 4.0 e del controllo ObjectDataSource, parte 2: Aggiunta di un livello di logica di business e unit test

di Tom Dykstra

Questa serie di esercitazioni si basa sull'applicazione Web Contoso University creata dal Introduzione con la serie di esercitazioni di Entity Framework 4.0. Se non sono state completate le esercitazioni precedenti, come punto di partenza per questa esercitazione è possibile scaricare l'applicazione creata. È anche possibile scaricare l'applicazione creata dalla serie completa di esercitazioni. Per domande sulle esercitazioni, è possibile pubblicarle nel forum di ASP.NET Entity Framework.

Nell'esercitazione precedente è stata creata un'applicazione Web a più livelli usando Entity Framework e il ObjectDataSource controllo . Questa esercitazione illustra come aggiungere logica di business mantenendo separato il livello BLL (Business Logic Layer) e il livello di accesso ai dati (DAL) e illustra come creare unit test automatizzati per BLL.

In questa esercitazione verranno completate le attività seguenti:

  • Creare un'interfaccia del repository che dichiara i metodi di accesso ai dati necessari.
  • Implementare l'interfaccia del repository nella classe del repository.
  • Creare una classe della logica di business che chiama la classe del repository per eseguire le funzioni di accesso ai dati.
  • Connettere il ObjectDataSource controllo alla classe della logica di business anziché alla classe del repository.
  • Creare un progetto unit test e una classe di repository che usa raccolte in memoria per l'archivio dati.
  • Creare uno unit test per la logica di business che si vuole aggiungere alla classe della logica di business, quindi eseguire il test e verificarne l'esito negativo.
  • Implementare la logica di business nella classe della logica di business, quindi ripetere lo unit test e vederla superata.

Si useranno le pagine Departments.aspx e DepartmentsAdd.aspx create nell'esercitazione precedente.

Creazione di un'interfaccia del repository

Si inizierà creando l'interfaccia del repository.

Immagine08

Nella cartella DAL creare un nuovo file di classe, denominarlo ISchoolRepository.cs e sostituire il codice esistente con il codice seguente:

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'interfaccia definisce un metodo per ognuno dei metodi CRUD (create, read, update, delete) creati nella classe del repository.

SchoolRepository Nella classe in SchoolRepository.cs indicare che questa classe implementa l'interfaccia ISchoolRepository :

public class SchoolRepository : IDisposable, ISchoolRepository

Creazione di una classe Business-Logic

Si creerà quindi la classe della logica di business. A tale scopo, è possibile aggiungere la logica di business che verrà eseguita dal ObjectDataSource controllo, anche se questa operazione non verrà ancora eseguita. Per il momento, la nuova classe della logica di business eseguirà solo le stesse operazioni CRUD eseguite dal repository.

Immagine09

Creare una nuova cartella e denominarla BLL. In un'applicazione reale, il livello della logica di business viene in genere implementato come libreria di classi, un progetto separato, ma per mantenere semplice questa esercitazione, le classi BLL verranno mantenute in una cartella del progetto.

Nella cartella BLL creare un nuovo file di classe, denominarlo SchoolBL.cs e sostituire il codice esistente con il codice seguente:

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

    }
}

Questo codice crea gli stessi metodi CRUD illustrati in precedenza nella classe del repository, ma anziché accedere direttamente ai metodi di Entity Framework, chiama i metodi della classe del repository.

La variabile di classe che contiene un riferimento alla classe del repository è definita come tipo di interfaccia e il codice che crea un'istanza della classe del repository è contenuto in due costruttori. Il costruttore senza parametri verrà utilizzato dal ObjectDataSource controllo . Crea un'istanza della SchoolRepository classe creata in precedenza. L'altro costruttore consente a qualsiasi codice che crea un'istanza della classe della logica di business di passare qualsiasi oggetto che implementa l'interfaccia del repository.

I metodi CRUD che chiamano la classe repository e i due costruttori consentono di usare la classe della logica di business con qualsiasi archivio dati back-end scelto. La classe della logica di business non deve essere a conoscenza del modo in cui la classe che sta chiamando rende persistenti i dati. (Questo è spesso chiamato ignoranza di persistenza. Ciò semplifica gli unit test, perché è possibile connettere la classe della logica di business a un'implementazione del repository che usa un elemento semplice come le raccolte in memoria List per archiviare i dati.

Nota

Tecnicamente, gli oggetti entità non sono ancora ignoranti persistenza, perché vengono create istanze da classi che ereditano dalla classe di EntityObject Entity Framework. Per ignorare completamente la persistenza, è possibile usare oggetti CLR precedenti o POCO, al posto di oggetti che ereditano dalla EntityObject classe . L'uso di poCOs esula dall'ambito di questa esercitazione. Per altre informazioni, vedere Testability and Entity Framework 4.0 (Testabilità ed Entity Framework 4.0 ) nel sito Web MSDN.

È ora possibile connettere i ObjectDataSource controlli alla classe della logica di business anziché al repository e verificare che tutto funzioni come in precedenza.

In Departments.aspx e DepartmentsAdd.aspx modificare ogni occorrenza di TypeName="ContosoUniversity.DAL.SchoolRepository" in TypeName="ContosoUniversity.BLL.SchoolBL". Sono presenti quattro istanze in tutte.

Eseguire le pagine Departments.aspx e DepartmentsAdd.aspx per verificare che funzionino ancora come in precedenza.

Immagine01

Immagine02

Creazione di un progetto e un'implementazione del repository di Unit-Test

Aggiungere un nuovo progetto alla soluzione usando il modello Progetto di test e denominarlo ContosoUniversity.Tests.

Nel progetto di test aggiungere un riferimento a System.Data.Entity e aggiungere un riferimento al ContosoUniversity progetto.

È ora possibile creare la classe del repository che verrà usata con gli unit test. L'archivio dati per questo repository sarà all'interno della classe .

Immagine12

Nel progetto di test creare un nuovo file di classe, denominarlo MockSchoolRepository.cs e sostituire il codice esistente con il codice seguente:

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

Questa classe di repository ha gli stessi metodi CRUD di quello che accede direttamente a Entity Framework, ma funzionano con List le raccolte in memoria anziché con un database. In questo modo è più semplice configurare e convalidare unit test per la classe della logica di business.

Creazione di unit test

Il modello di progetto Test ha creato automaticamente una classe di unit test stub e l'attività successiva consiste nel modificare questa classe aggiungendo metodi di unit test per la logica di business da aggiungere alla classe della logica di business.

Immagine13

In Contoso University qualsiasi insegnante singolo può essere solo l'amministratore di un singolo reparto ed è necessario aggiungere la logica di business per applicare questa regola. Si inizierà aggiungendo test ed eseguendo i test per verificarne l'esito negativo. Si aggiungerà quindi il codice ed eseguirà di nuovo i test, prevedendo che vengano superati.

Aprire il file UnitTest1.cs e aggiungere using istruzioni per la logica di business e i livelli di accesso ai dati creati nel progetto ContosoUniversity:

using ContosoUniversity.BLL;
using ContosoUniversity.DAL;

Sostituire il TestMethod1 metodo con i metodi seguenti:

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

Il CreateSchoolBL metodo crea un'istanza della classe del repository creata per il progetto di unit test, che quindi passa a una nuova istanza della classe della logica di business. Il metodo usa quindi la classe della logica di business per inserire tre reparti che è possibile usare nei metodi di test.

I metodi di test verificano che la classe della logica di business generi un'eccezione se un utente tenta di inserire un nuovo reparto con lo stesso amministratore di un reparto esistente o se un utente tenta di aggiornare l'amministratore di un reparto impostandolo sull'ID di una persona che è già l'amministratore di un altro reparto.

Non è ancora stata creata la classe di eccezione, quindi questo codice non verrà compilato. Per eseguire la compilazione, fare clic con il pulsante destro del mouse DuplicateAdministratorException e scegliere Genera e quindi Classe.

Screenshot che mostra l'opzione Genera selezionata nel sottomenu Classe.

Verrà creata una classe nel progetto di test che è possibile eliminare dopo aver creato la classe di eccezione nel progetto principale. e ha implementato la logica di business.

Eseguire il progetto di test. Come previsto, i test hanno esito negativo.

Immagine03

Aggiunta della logica di business per effettuare un test pass

Successivamente, si implementerà la logica di business che rende impossibile impostare come amministratore di un reparto qualcuno che è già amministratore di un altro reparto. Verrà generata un'eccezione dal livello della logica di business e quindi intercettata nel livello presentazione se un utente modifica un reparto e fa clic su Aggiorna dopo aver selezionato un utente che è già un amministratore. È anche possibile rimuovere gli insegnanti dall'elenco a discesa che sono già amministratori prima di eseguire il rendering della pagina, ma lo scopo di questo articolo è quello di usare il livello della logica di business.

Per iniziare, creare la classe di eccezione che verrà generata quando un utente tenta di rendere un insegnante l'amministratore di più di un reparto. Nel progetto principale creare un nuovo file di classe nella cartella BLL , denominarlo DuplicateAdministratorException.cs e sostituire il codice esistente con il codice seguente:

using System;

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

Eliminare ora il file DuplicateAdministratorException.cs temporaneo creato in precedenza nel progetto di test per poter essere compilato.

Nel progetto principale aprire il file SchoolBL.cs e aggiungere il metodo seguente che contiene la logica di convalida. Il codice fa riferimento a un metodo che verrà creato in un secondo momento.

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

Questo metodo verrà chiamato quando si inseriscono o si aggiornano Department le entità per verificare se un altro reparto ha già lo stesso amministratore.

Il codice chiama un metodo per cercare nel database un'entità Department con lo stesso Administrator valore della proprietà dell'entità da inserire o aggiornare. Se ne viene trovato uno, il codice genera un'eccezione. Non è necessario alcun controllo di convalida se l'entità inserita o aggiornata non ha alcun Administrator valore e non viene generata alcuna eccezione se il metodo viene chiamato durante un aggiornamento e l'entità Department trovata corrisponde all'entità Department da aggiornare.

Chiamare il nuovo metodo dai Insert metodi e Update :

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

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

In ISchoolRepository.cs aggiungere la dichiarazione seguente per il nuovo metodo di accesso ai dati:

IEnumerable<Department> GetDepartmentsByAdministrator(Int32 administrator);

In SchoolRepository.cs aggiungere l'istruzione seguente using :

using System.Data.Objects;

In SchoolRepository.cs aggiungere il nuovo metodo di accesso ai dati seguente:

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

Questo codice recupera le Department entità con un amministratore specificato. Dovrebbe essere trovato un solo reparto (se presente). Tuttavia, poiché nel database non è incluso alcun vincolo, il tipo restituito è una raccolta nel caso in cui vengano trovati più reparti.

Per impostazione predefinita, quando il contesto dell'oggetto recupera le entità dal database, tiene traccia di tali entità nel gestore dello stato dell'oggetto. Il MergeOption.NoTracking parametro specifica che questo rilevamento non verrà eseguito per questa query. Ciò è necessario perché la query potrebbe restituire l'entità esatta che si sta tentando di aggiornare e quindi non sarebbe possibile collegare tale entità. Ad esempio, se si modifica il reparto Cronologia nella pagina Departments.aspx e si lascia invariato l'amministratore, questa query restituirà il reparto Cronologia. Se NoTracking non è impostato, il contesto dell'oggetto avrà già l'entità reparto Cronologia nel gestore dello stato dell'oggetto. Quando quindi si collega l'entità reparto cronologia ricreata dallo stato di visualizzazione, il contesto dell'oggetto genererà un'eccezione che indica "An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key".

In alternativa a specificare MergeOption.NoTracking, è possibile creare un nuovo contesto di oggetto solo per questa query. Poiché il nuovo contesto dell'oggetto avrebbe un proprio gestore dello stato dell'oggetto, non vi sarebbe alcun conflitto quando si chiama il Attach metodo . Il nuovo contesto dell'oggetto condividerà i metadati e la connessione al database con il contesto dell'oggetto originale, pertanto la riduzione delle prestazioni di questo approccio alternativo sarebbe minima. L'approccio illustrato qui, tuttavia, introduce l'opzione NoTracking , che risulta utile in altri contesti. L'opzione NoTracking è illustrata più avanti in un'esercitazione successiva di questa serie.

Nel progetto di test aggiungere il nuovo metodo di accesso ai dati a MockSchoolRepository.cs:

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

Questo codice usa LINQ per eseguire la stessa selezione dei dati utilizzata dal repository del ContosoUniversity progetto LINQ to Entities.

Eseguire di nuovo il progetto di test. Questa volta il test ha esito positivo.

Immagine04

Gestione delle eccezioni ObjectDataSource

ContosoUniversity Nel progetto eseguire la pagina Departments.aspx e provare a modificare l'amministratore di un reparto in un utente che è già amministratore di un altro reparto. Tenere presente che è possibile modificare solo i reparti aggiunti durante questa esercitazione, perché il database viene precaricato con dati non validi. Viene visualizzata la pagina di errore del server seguente:

Immagine05

Non si vuole che gli utenti visualizzino questo tipo di pagina di errore, quindi è necessario aggiungere codice di gestione degli errori. Aprire Departments.aspx e specificare un gestore per l'evento OnUpdated di DepartmentsObjectDataSource. Il ObjectDataSource tag di apertura è ora simile all'esempio seguente.

<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" >

In Departments.aspx.cs aggiungere l'istruzione seguente using :

using ContosoUniversity.BLL;

Aggiungere il gestore seguente per l'evento 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;
        }
    }
}

Se il ObjectDataSource controllo rileva un'eccezione quando tenta di eseguire l'aggiornamento, passa l'eccezione nell'argomento dell'evento (e) a questo gestore. Il codice nel gestore verifica se l'eccezione è l'eccezione di amministratore duplicato. In caso affermativo, il codice crea un controllo validator contenente un messaggio di errore che il ValidationSummary controllo deve visualizzare.

Eseguire la pagina e tentare di eseguire nuovamente l'amministratore di due reparti. Questa volta il ValidationSummary controllo visualizza un messaggio di errore.

Image06

Apportare modifiche simili alla pagina DepartmentsAdd.aspx . In DepartmentsAdd.aspx specificare un gestore per l'evento OnInserted di DepartmentsObjectDataSource. Il markup risultante sarà simile all'esempio seguente.

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

In DepartmentsAdd.aspx.cs aggiungere la stessa using istruzione:

using ContosoUniversity.BLL;

Aggiungere il gestore eventi seguente:

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

È ora possibile testare la pagina DepartmentsAdd.aspx.cs per verificare che gestisca correttamente anche i tentativi di eseguire un'unica persona che sia l'amministratore di più di un reparto.

Questa operazione completa l'introduzione all'implementazione del modello di repository per l'uso del ObjectDataSource controllo con Entity Framework. Per altre informazioni sul modello di repository e sulla testabilità, vedere il white paper MSDN Testability e Entity Framework 4.0.

Nell'esercitazione seguente verrà illustrato come aggiungere funzionalità di ordinamento e filtro all'applicazione.