Uso de Entity Framework 4.0 y el control ObjectDataSource, parte 2: adición de una capa de lógica empresarial y pruebas unitarias

Por Tom Dykstra

Esta serie de tutoriales se basa en la aplicación web Contoso University, creada por la serie de tutoriales Introducción a Entity Framework 4.0. Si no completó los tutoriales anteriores, como punto de partida para este tutorial, puede descargar la aplicación que habría creado. También puede descargar la aplicación creada por la serie de tutoriales completa. Si tiene preguntas sobre los tutoriales, puede publicarlas en el foro de ASP.NET Entity Framework.

En el tutorial anterior, creó una aplicación web de n niveles mediante Entity Framework y el control ObjectDataSource. En este tutorial se muestra cómo agregar lógica empresarial al mantener separada la capa de lógica empresarial (BLL) y la capa de acceso a datos (DAL) y se muestra cómo crear pruebas unitarias automatizadas para la BLL.

En este tutorial completará las siguientes tareas:

  • Creación de una interfaz de repositorio que declare los métodos de acceso a datos que necesita.
  • Implementación de la interfaz del repositorio en la clase de repositorio.
  • Creación de una clase de lógica empresarial que llame a la clase de repositorio para realizar funciones de acceso a datos.
  • Conexión del control ObjectDataSource a la clase de lógica empresarial en lugar de a la clase de repositorio.
  • Creación de un proyecto de prueba unitaria y una clase de repositorio que use colecciones en memoria para su almacén de datos.
  • Cree una prueba unitaria para la lógica empresarial que quiera agregar a la clase de lógica empresarial y, a continuación, ejecute la prueba y vea que se produce un error.
  • Implemente la lógica empresarial en la clase de lógica empresarial y vuelva a ejecutar la prueba unitaria y vea que se supera.

Trabajará con las páginas Departments.aspx y DepartmentsAdd.aspx que creó en el tutorial anterior.

Creación de una interfaz de repositorio

Comenzará creando la interfaz del repositorio.

Image08

En la carpeta DAL, cree un nuevo archivo de clase, asígnele el nombre ISchoolRepository.cs y reemplace el código existente por el siguiente:

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

La interfaz define un método para cada uno de los métodos CRUD (crear, leer, actualizar, eliminar) que creó en la clase de repositorio.

En la clase SchoolRepository de SchoolRepository.cs, indique que esta clase implementa la interfaz ISchoolRepository:

public class SchoolRepository : IDisposable, ISchoolRepository

Creación de una clase de lógica empresarial

A continuación, creará la clase de lógica empresarial. Para ello, puede agregar lógica empresarial que ejecutará el control ObjectDataSource, aunque aún no lo hará. Por ahora, la nueva clase de lógica empresarial solo realizará las mismas operaciones CRUD que realiza el repositorio.

Image09

Cree una carpeta y asígnela el nombre BLL. (En una aplicación real, la capa de lógica empresarial se implementaría típicamente como una biblioteca de clases [un proyecto separado] pero para mantener este tutorial simple, las clases BLL se mantendrán en una carpeta del proyecto).

En la carpeta BLL, cree un nuevo archivo de clase, asígnele el nombre SchoolBL.cs y reemplace el código existente por el siguiente:

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

    }
}

Este código crea los mismos métodos CRUD que vio anteriormente en la clase de repositorio, pero en lugar de acceder a los métodos de Entity Framework directamente, llama a los métodos de clase de repositorio.

La variable de clase que contiene una referencia a la clase de repositorio se define como un tipo de interfaz y el código que crea instancias de la clase de repositorio se encuentra en dos constructores. El control ObjectDataSource usará el constructor sin parámetros. Crea una instancia de la clase SchoolRepository que creó anteriormente. El otro constructor permite cualquier código que cree una instancia de la clase de lógica empresarial para pasar cualquier objeto que implemente la interfaz del repositorio.

Los métodos CRUD que llaman a la clase de repositorio y los dos constructores permiten usar la clase de lógica empresarial con cualquier almacén de datos back-end que elija. La clase de lógica empresarial no necesita tener en cuenta cómo la clase a la que llama conserva los datos. (Esto suele denominar omisión de persistencia). Esto facilita las pruebas unitarias, ya que puede conectar la clase de lógica empresarial a una implementación de repositorio que usa algo tan sencillo como colecciones en memoria List para almacenar datos.

Nota:

Técnicamente, los objetos de entidad todavía no omiten la persistencia, ya que se crean instancias de las clases que heredan de la clase EntityObject de Entity Framework. Para una completa omisión de persistencia, puede usar objetos CLR antiguos sin formato o POCO, en lugar de objetos que heredan de la clase EntityObject. El uso de POCO está fuera del ámbito de este tutorial. Para obtener más información, consulte Capacidad de pruebas y Entity Framework 4.0 en el sitio web de MSDN).

Ahora puede conectar los controles ObjectDataSource a la clase de lógica empresarial en lugar del repositorio y comprobar que todo funciona como lo hizo antes.

En Departments.aspx y DepartmentsAdd.aspx, cambie cada aparición de TypeName="ContosoUniversity.DAL.SchoolRepository" a TypeName="ContosoUniversity.BLL.SchoolBL". (Hay cuatro instancias en total).

Ejecute las páginas Departments.aspx y DepartmentsAdd.aspx para comprobar que siguen funcionando como lo hacían antes.

Image01

Image02

Creación de un proyecto de prueba unitaria e implementación de repositorio

Agregue un nuevo proyecto a la solución mediante la plantilla Proyecto de prueba y asígnele el nombre ContosoUniversity.Tests.

En el proyecto de prueba, agregue una referencia a System.Data.Entity y agregue una referencia de proyecto al proyecto ContosoUniversity.

Ahora puede crear la clase de repositorio que usará con pruebas unitarias. El almacén de datos de este repositorio estará dentro de la clase.

Image12

En el proyecto de prueba, cree un nuevo archivo de clase, asígnele el nombre MockSchoolRepository.cs y reemplace el código existente por el siguiente:

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

Esta clase de repositorio tiene los mismos métodos CRUD que el que accede directamente a Entity Framework, pero funcionan con colecciones List en memoria en lugar de con una base de datos. Esto facilita que una clase de prueba configure y valide pruebas unitarias para la clase de lógica empresarial.

Crear pruebas unitarias

La plantilla de proyecto Prueba creó una clase de prueba unitaria de código auxiliar, y la siguiente tarea consiste en modificar esta clase agregando métodos de prueba unitaria a ella para la lógica empresarial que desea agregar a la clase de lógica empresarial.

Image13

En Contoso University, cualquier instructor individual solo puede ser el administrador de un único departamento y debe agregar lógica empresarial para aplicar esta regla. Para empezar, agregará pruebas y las ejecutará para ver cómo fallan. A continuación, agregará el código y volverá a ejecutar las pruebas, esperando que se superen.

Abra el archivo UnitTest1.cs y agregue instrucciones using para las capas de lógica empresarial y acceso a datos que creó en el proyecto ContosoUniversity:

using ContosoUniversity.BLL;
using ContosoUniversity.DAL;

Reemplace el método TestMethod1 por los siguientes:

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

El método CreateSchoolBL crea una instancia de la clase de repositorio que creó para el proyecto de prueba unitaria, que luego pasa a una nueva instancia de la clase de lógica empresarial. A continuación, el método usa la clase de lógica empresarial para insertar tres departamentos que puede usar en métodos de prueba.

Los métodos de prueba comprueban que la clase de lógica empresarial produce una excepción si alguien intenta insertar un nuevo departamento con el mismo administrador que un departamento existente o si alguien intenta actualizar el administrador de un departamento estableciendo en el identificador de una persona que ya es el administrador de otro departamento.

Aún no ha creado la clase de excepción, por lo que este código no se compilará. Para que se compile, haga clic con el botón derecho en DuplicateAdministratorException y seleccione Generar y, a continuación, Clase.

Screenshot that shows Generate selected in the Class submenu.

Esto crea una clase en el proyecto de prueba que puede eliminar después de crear la clase de excepción en el proyecto principal. e implementar la lógica empresarial.

Ejecute el proyecto de prueba. Como se esperaba, se produce un error en las pruebas.

Image03

Adición de lógica empresarial para realizar una prueba superada

A continuación, implementará la lógica empresarial que hace que sea imposible establecer como administrador de un departamento alguien que ya sea administrador de otro departamento. Producirá una excepción de la capa de lógica empresarial y, a continuación, la detectará en la capa de presentación si un usuario edita un departamento y hace clic en Actualizar después de seleccionar a alguien que ya es administrador. (También puede quitar instructores de la lista desplegable que ya son administradores antes de representar la página, pero el propósito aquí es trabajar con la capa de lógica empresarial).

Empiece por crear la clase de excepción que iniciará cuando un usuario intente convertir a un instructor en el administrador de más de un departamento. En el proyecto principal, cree un nuevo archivo de clase en la carpeta BLL, asígnele el nombre DuplicateAdministratorException.cs y reemplace el código existente por el siguiente:

using System;

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

Ahora elimine el archivo de DuplicateAdministratorException.cs temporal que creó anteriormente en el proyecto de prueba para poder compilarlo.

En el proyecto principal, abra el archivo SchoolBL.cs y agregue el siguiente método que contiene la lógica de validación. (El código hace referencia a un método que creará más adelante).

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

Llamará a este método al insertar o actualizar entidades Department para comprobar si otro departamento ya tiene el mismo administrador.

El código llama a un método para buscar la base de datos de una entidad Department que tenga el mismo valor de propiedad Administrator que la entidad que se va a insertar o actualizar. Si se encuentra uno, el código produce una excepción. No se requiere ninguna comprobación de validación si la entidad que se inserta o actualiza no tiene ningún valor Administrator y no se produce ninguna excepción si se llama al método durante una actualización y la entidad Department encontrada coincide con la entidad Department que se está actualizando.

Llame al nuevo método desde los métodos Insert y Update:

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

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

En ISchoolRepository.cs, agregue la siguiente declaración para el nuevo método de acceso a datos:

IEnumerable<Department> GetDepartmentsByAdministrator(Int32 administrator);

En SchoolRepository.cs, agregue la siguiente instrucción using:

using System.Data.Objects;

En SchoolRepository.cs, agregue el siguiente nuevo método de acceso a datos:

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

Este código recupera entidades Department que tienen un administrador especificado. Solo se debe encontrar un departamento (si existe). Sin embargo, dado que no hay ninguna restricción integrada en la base de datos, el tipo de valor devuelto es una colección en caso de que se encuentren varios departamentos.

De forma predeterminada, cuando el contexto del objeto recupera entidades de la base de datos, realiza un seguimiento de ellas en su administrador de estado de objetos. El parámetro MergeOption.NoTracking especifica que este seguimiento no se realizará para esta consulta. Esto es necesario porque la consulta puede devolver la entidad exacta que está intentando actualizar y, a continuación, no podrá adjuntar esa entidad. Por ejemplo, si edita el departamento historial en la página de Departments.aspx y deja el administrador sin cambios, esta consulta devolverá el departamento historial. Si no se establece NoTracking, el contexto del objeto ya tendría la entidad Departamento de historial en su administrador de estado de objetos. A continuación, al adjuntar la entidad departamento de historial que se vuelve a crear a partir del estado de vista, el contexto del objeto produciría una excepción que indica "An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key".

(Como alternativa a especificar MergeOption.NoTracking, podría crear un nuevo contexto de objeto solo para esta consulta. Dado que el nuevo contexto de objeto tendría su propio administrador de estado de objetos, no habría ningún conflicto al llamar al método Attach. El nuevo contexto de objeto compartiría metadatos y conexión de base de datos con el contexto de objeto original, por lo que la penalización de rendimiento de este enfoque alternativo sería mínima. Sin embargo, el enfoque que se muestra aquí presenta la opción NoTracking, que encontrará útil en otros contextos. La opción NoTracking se describe más adelante en un tutorial posterior de esta serie).

En el proyecto de prueba, agregue el nuevo método de acceso a datos a MockSchoolRepository.cs:

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

Este código usa LINQ para realizar la misma selección de datos para la que el repositorio de proyectos ContosoUniversity usa LINQ to Entities.

Vuelva a ejecutar el proyecto de prueba. Esta vez las pruebas son correctas.

Image04

Control de excepciones ObjectDataSource

En el proyecto ContosoUniversity, ejecute la página Departments.aspx e intente cambiar el administrador de un departamento a alguien que ya sea administrador para otro departamento. (Recuerde que solo puede editar los departamentos que agregó durante este tutorial, ya que la base de datos se carga previamente con datos no válidos). Obtendrá la siguiente página de error del servidor:

Image05

No desea que los usuarios vean este tipo de página de error, por lo que debe agregar código de control de errores. Abra Departments.aspx y especifique un controlador para el evento OnUpdated del DepartmentsObjectDataSource. La etiqueta de apertura ObjectDataSource ahora es similar al ejemplo siguiente.

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

En Departments.aspx.cs, agregue la siguiente instrucción using:

using ContosoUniversity.BLL;

Agregue el controlador siguiente para el 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;
        }
    }
}

Si el control ObjectDataSource detecta una excepción cuando intenta realizar la actualización, pasa la excepción en el argumento de evento (e) a este controlador. El código del controlador comprueba si la excepción es la excepción de administrador duplicada. Si es así, el código crea un control de validador que contiene un mensaje de error para que se muestre el control ValidationSummary.

Ejecute la página e intente convertir a alguien en el administrador de dos departamentos de nuevo. Esta vez, el control ValidationSummary muestra un mensaje de error.

Image06

Realice cambios similares en la página DepartmentsAdd.aspx. En DepartmentsAdd.aspx, especifique un controlador para el evento OnInserted del DepartmentsObjectDataSource. El marcado resultante será similar al ejemplo siguiente.

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

En DepartmentsAdd.aspx.cs, agregue la misma instrucción using:

using ContosoUniversity.BLL;

Agregue el siguiente controlador de eventos:

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

Ahora puede probar la página de DepartmentsAdd.aspx.cs para comprobar que también controla correctamente los intentos de realizar a una persona el administrador de más de un departamento.

Esto completa la introducción a la implementación del patrón de repositorio para usar el control ObjectDataSource con Entity Framework. Para obtener más información sobre el patrón de repositorio y la capacidad de prueba, consulte las notas del producto de MSDN Capacidad de prueba y Entity Framework 4.0.

En el siguiente tutorial, verá cómo agregar funcionalidades de ordenación y filtrado a la aplicación.