Compartir vía


Implementación de los patrones de repositorio y unidad de trabajo en una aplicación ASP.NET MVC (9 de 10)

Por Tom Dykstra

En la aplicación web de ejemplo Contoso University se muestra cómo crear aplicaciones de ASP.NET MVC 4 con Code First de Entity Framework 5 y Visual Studio 2012. Para obtener información sobre la serie de tutoriales, consulte el primer tutorial de la serie.

Nota:

Si se encuentra con un problema que no puede resolver, descargue el capítulo completado e intente reproducir el problema. Por lo general, puede encontrar la solución al problema si compara el código con el código completado. Para obtener algunos errores comunes y cómo resolverlos, vea Errores y soluciones alternativas.

En el tutorial anterior ha usado la herencia para reducir el código redundante en las clases de entidad Student y Instructor. En este tutorial verá algunas maneras de usar los patrones de repositorio y unidad de trabajo para las operaciones CRUD. Como en el tutorial anterior, en este cambiará la forma en que el código funciona con páginas que ya ha creado en lugar de crear otras.

Patrones de repositorio y unidad de trabajo

Los patrones de repositorio y unidad de trabajo están previstos para crear una capa de abstracción entre la capa de acceso de datos y la capa de lógica de negocios de una aplicación. Implementar estos patrones puede ayudar a aislar la aplicación de cambios en el almacén de datos y puede facilitar la realización de pruebas unitarias automatizadas o el desarrollo controlado por pruebas (TDD).

En este tutorial implementará una clase de repositorio para cada tipo de entidad. Para el tipo de entidad Student, creará una interfaz de repositorio y una clase de repositorio. Al crear una instancia del repositorio en el controlador, usará la interfaz para que el controlador acepte una referencia a cualquier objeto que implemente la interfaz del repositorio. Cuando el controlador se ejecuta en un servidor web, recibe un repositorio que funciona con Entity Framework. Cuando el controlador se ejecuta en una clase de prueba unitaria, recibe un repositorio que funciona con los datos almacenados de una manera que se pueden manipular fácilmente para las pruebas, como una colección en memoria.

Más adelante en el tutorial usará varios repositorios y una unidad de clase de trabajo para los tipos de entidad Course y Department en el controlador Course. La unidad de clase de trabajo coordina el trabajo de varios repositorios mediante la creación de una sola clase de contexto de base de datos compartida por todos ellos. Si quisiera poder realizar pruebas unitarias automatizadas, tendría que crear y usar interfaces para estas clases de la misma manera que hizo para el repositorio Student. Pero para simplificar el tutorial, creará y usará estas clases sin interfaces.

En la ilustración siguiente se muestra una manera de conceptualizar las relaciones entre el controlador y las clases de contexto en comparación con no usar el repositorio o la unidad de patrón de trabajo en absoluto.

Repository_pattern_diagram

No creará pruebas unitarias en esta serie de tutoriales. Para obtener una introducción a TDD con una aplicación MVC que usa el patrón de repositorio, vea Tutorial: Uso de TDD con ASP.NET MVC. Para más información sobre el patrón de repositorio, vea los siguientes recursos:

Nota:

Hay muchas maneras de implementar los patrones de repositorio y unidad de trabajo. Puede usar clases de repositorio con o sin una unidad de clase de trabajo. Puede implementar un único repositorio para todos los tipos de entidad, o bien uno para cada tipo. Si implementa uno para cada tipo, puede usar clases independientes, una clase base genérica y clases derivadas, o bien una clase base abstracta y clases derivadas. Puede incluir lógica de negocios en el repositorio o restringirlo a la lógica de acceso a datos. También puede crear una capa de abstracción en la clase de contexto de base de datos mediante interfaces IDbSeten lugar de tipos DbSet para los conjuntos de entidades. El enfoque para implementar una capa de abstracción que se muestra en este tutorial es una opción que puede tener en cuenta, no una recomendación para todos los escenarios y entornos.

Creación de la clase de repositorio Student

En la carpeta DAL, cree un archivo de clase con el nombre IStudentRepository.cs y reemplace el código existente por el siguiente:

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

Este código declara un conjunto típico de métodos CRUD, incluidos dos métodos de lectura, uno que devuelve todas las Student entidades y otro que busca una sola entidad Student por identificador.

En la carpeta DAL, cree un archivo de clase con el nombre StudentRepository.cs. Reemplace el código existente por el código siguiente, que implementa la interfaz 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);
        }
    }
}

El contexto de la base de datos se define en una variable de clase y el constructor espera que el objeto que realiza la llamada pase una instancia del contexto:

private SchoolContext context;

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

Podría crear una instancia de un nuevo contexto en el repositorio, pero si usara varios repositorios en un controlador, cada uno acabaría con un contexto independiente. Más adelante usará varios repositorios en el controlador Course y verá cómo una unidad de clase de trabajo puede asegurarse de que todos los repositorios usen el mismo contexto.

El repositorio implementa IDisposable y elimina el contexto de la base de datos como vio anteriormente en el controlador, y sus métodos CRUD realizan llamadas al contexto de la base de datos de la misma manera que antes.

Cambio del controlador Student para usar el repositorio

En StudentController.cs, reemplace el código que se encuentra actualmente en la clase por el código siguiente. Los cambios aparecen resaltados.

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

Ahora el controlador declara una variable de clase para un objeto que implementa la interfaz IStudentRepository en lugar de la clase de contexto:

private IStudentRepository studentRepository;

El constructor predeterminado (sin parámetros) crea una instancia del contexto y un constructor opcional permite que el autor de la llamada pase una instancia del contexto.

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

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

(Si usara la inserción de dependencias o DI, no necesitaría el constructor predeterminado porque el software de DI aseguraría que siempre se proporcione el objeto de repositorio correcto).

En los métodos CRUD, ahora se llama al repositorio en lugar del contexto:

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

Y el método Dispose ahora elimina el repositorio en lugar del contexto:

studentRepository.Dispose();

Ejecute el sitio y haga clic en la pestaña Students.

Students_Index_page

La página tiene el mismo aspecto y funciona igual que antes de cambiar el código para usar el repositorio y el funcionamiento de las otras páginas Student también es igual. Pero hay una diferencia importante en la forma en la que el método Index del controlador realiza el filtrado y la ordenación. La versión original de este método contenía el código siguiente:

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

El método Index actualizado contiene el código siguiente:

var students = from s in studentRepository.GetStudents()
                select s;
if (!String.IsNullOrEmpty(searchString))
{
    students = students.Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())
                        || s.FirstMidName.ToUpper().Contains(searchString.ToUpper()));
}

Solo ha cambiado el código resaltado.

En la versión original del código, students tiene el tipo de un objeto IQueryable. La consulta no se envía a la base de datos hasta que se convierte en una colección mediante un método como ToList, que no aparece hasta que la vista Índice tiene acceso al modelo de alumno. El método Where del código original anterior se convierte en una cláusula WHERE de la consulta SQL que se envía a la base de datos. Esto significa, a su vez, que la base de datos solo devuelve las entidades seleccionadas. Pero como resultado de cambiar context.Students a studentRepository.GetStudents(), la variable students después de esta instrucción es una colección IEnumerable que incluye todos los alumnos de la base de datos. El resultado final de aplicar el método Where es el mismo, pero ahora el trabajo se realiza en memoria en el servidor web y no en la base de datos. En el caso de las consultas que devuelven grandes volúmenes de datos, esto puede ser ineficaz.

Sugerencia

Diferencias entre IQueryable e IEnumerable

Después de implementar el repositorio como se muestra aquí, incluso si escribe algo en el cuadro Buscar, la consulta enviada a SQL Server devuelve todas las filas Student porque no incluye los criterios de búsqueda:

Screenshot of the code that shows the new student repository implemented and highlighted.

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'

Esta consulta devuelve todos los datos de los alumnos porque el repositorio ha ejecutado la consulta sin conocer los criterios de búsqueda. El proceso de ordenación, aplicación de criterios de búsqueda y selección de un subconjunto de los datos para la paginación (que, en este caso, solo muestra tres filas) se realiza en memoria más adelante cuando se llama al método ToPagedList en la colección IEnumerable.

En la versión anterior del código (antes de implementar el repositorio) la consulta no se envía a la base de datos hasta después de aplicar los criterios de búsqueda, cuando se llama a ToPagedList en el objeto IQueryable.

Screenshot that shows the Student Controller code. A search string row of code and the To Paged List row of code are highlighted.

Cuando se llama a ToPagedList en un objeto IQueryable, la consulta enviada a SQL Server especifica la cadena de búsqueda y, como resultado, solo se devuelven las filas que cumplen los criterios de búsqueda y no es necesario realizar ningún filtrado en memoria.

exec sp_executesql N'SELECT TOP (3) 
[Project1].[StudentID] AS [StudentID], 
[Project1].[LastName] AS [LastName], 
[Project1].[FirstName] AS [FirstName], 
[Project1].[EnrollmentDate] AS [EnrollmentDate]
FROM ( SELECT [Project1].[StudentID] AS [StudentID], [Project1].[LastName] AS [LastName], [Project1].[FirstName] AS [FirstName], [Project1].[EnrollmentDate] AS [EnrollmentDate], row_number() OVER (ORDER BY [Project1].[LastName] ASC) AS [row_number]
FROM ( SELECT 
    [Extent1].[StudentID] AS [StudentID], 
    [Extent1].[LastName] AS [LastName], 
    [Extent1].[FirstName] AS [FirstName], 
    [Extent1].[EnrollmentDate] AS [EnrollmentDate]
    FROM [dbo].[Student] AS [Extent1]
    WHERE (( CAST(CHARINDEX(UPPER(@p__linq__0), UPPER([Extent1].[LastName])) AS int)) > 0) OR (( CAST(CHARINDEX(UPPER(@p__linq__1), UPPER([Extent1].[FirstName])) AS int)) > 0)
)  AS [Project1]
)  AS [Project1]
WHERE [Project1].[row_number] > 0
ORDER BY [Project1].[LastName] ASC',N'@p__linq__0 nvarchar(4000),@p__linq__1 nvarchar(4000)',@p__linq__0=N'Alex',@p__linq__1=N'Alex'

(En el tutorial siguiente se explica cómo examinar las consultas enviadas a SQL Server).

En la sección siguiente se muestra cómo implementar métodos de repositorio que le permiten especificar que la base de datos debe realizar este trabajo.

Ahora ha creado una capa de abstracción entre el controlador y el contexto de base de datos de Entity Framework. Si realizara pruebas unitarias automatizadas con esta aplicación, podría crear una clase de repositorio alternativa en un proyecto de prueba unitaria que implemente IStudentRepository. En lugar de llamar al contexto para leer y escribir datos, esta clase de repositorio ficticia podría manipular colecciones en memoria para probar las funciones del controlador.

Implementación de un repositorio genérico y una clase de unidad de trabajo

La creación de una clase de repositorio para cada tipo de entidad podría dar lugar a una gran cantidad de código redundante y generar actualizaciones parciales. Por ejemplo, imagine que tiene que actualizar dos tipos de entidad diferentes como parte de la misma transacción. Si en cada una se usa una instancia de contexto de base de datos independiente, una podría ser correcta y en otra podría producirse un error. Una manera de minimizar el código redundante consiste en usar un repositorio genérico y una manera de asegurarse de que todos los repositorios usen el mismo contexto de base de datos (y, por tanto, coordinar todas las actualizaciones) consiste en usar una clase de unidad de trabajo.

En esta sección del tutorial, creará una clase GenericRepository y una clase UnitOfWork, y las usará en el controlador Course para acceder a los conjuntos de entidades Department y Course. Como se ha explicado antes, para simplificar esta parte del tutorial, no creará interfaces para estas clases. Pero si las usara para facilitar el TDD, normalmente las implementaría con interfaces de la misma manera que hizo con el repositorio Student.

Creación de un repositorio genérico

En la carpeta DAL, cree GenericRepository.cs y reemplace el código existente por el siguiente:

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

Las variables de clase se declaran para el contexto de la base de datos y para el conjunto de entidades para el que se crea la instancia del repositorio:

internal SchoolContext context;
internal DbSet dbSet;

El constructor acepta una instancia de contexto de base de datos e inicializa la variable de conjunto de entidades:

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

El método Get usa expresiones lambda para permitir que el código de llamada especifique una condición de filtro y una columna por la que ordenar los resultados, y un parámetro de cadena permite al autor de la llamada proporcionar una lista delimitada por comas de propiedades de navegación para la carga diligente:

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

El código Expression<Func<TEntity, bool>> filter significa que el autor de la llamada proporcionará una expresión lambda basada en el tipo TEntity y esta expresión devolverá un valor booleano. Por ejemplo, si se crea una instancia del repositorio para el tipo de entidad Student, el código del método que realiza la llamada podría especificar student => student.LastName == "Smith" para el parámetro filter.

El código Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy también significa que el autor de la llamada proporcionará una expresión lambda. Pero en este caso, la entrada a la expresión es un objeto IQueryable para el tipo TEntity. La expresión devolverá una versión ordenada de ese objeto IQueryable. Por ejemplo, si se crea una instancia del repositorio para el tipo de entidad Student, el código del método que realiza la llamada podría especificar q => q.OrderBy(s => s.LastName) para el parámetro orderBy.

El código del método Get crea un objeto IQueryable y, después, aplica la expresión de filtro si hay una:

IQueryable<TEntity> query = dbSet;

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

A continuación, aplica las expresiones de carga diligente después de analizar la lista delimitada por comas:

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

Por último, aplica la expresión orderBy si existe y devuelve los resultados; de lo contrario, devuelve los resultados de la consulta sin ordenar:

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

Al llamar al método Get, podría realizar el filtrado y la ordenación en la colección IEnumerable devuelta por el método en lugar de proporcionar parámetros para estas funciones. Pero el trabajo de ordenación y filtrado se realizaría en memoria en el servidor web. Al usar estos parámetros, se asegura de que la base de datos realiza el trabajo, no el servidor web. Una alternativa consiste en crear clases derivadas para tipos de entidad específicos y agregar métodos Get especializados, como GetStudentsInNameOrder o GetStudentsByName. Pero en una aplicación compleja, esto puede dar lugar a un gran número de clases derivadas y métodos especializados, lo que podría ser más trabajo que mantener.

El código de los métodos GetByID, Inserty Update es similar al que ha visto en el repositorio no genérico. (No proporciona un parámetro de carga diligente en la firma de GetByID, ya que no puede realizar la carga diligente con el método Find).

Se proporcionan dos sobrecargas para el método Delete:

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

Una solo permite pasar el identificador de la entidad que se va a eliminar y la otra toma una instancia de entidad. Como ha visto en el tutorial Control de la simultaneidad, para controlar la simultaneidad necesita un método Delete que tome una instancia de entidad que incluya el valor original de una propiedad de seguimiento.

Este repositorio genérico controlará los requisitos CRUD típicos. Cuando un tipo de entidad determinado tiene requisitos especiales, como filtrado u ordenación más complejos, puede crear una clase derivada que tenga métodos adicionales para ese tipo.

Creación de la clase de unidad de trabajo

La clase de unidad de trabajo tiene un único propósito: asegurarse de que cuando se usan varios repositorios, compartan un único contexto de base de datos. De este modo, cuando se completa una unidad de trabajo, puede llamar al método SaveChanges en esa instancia del contexto con la garantía de que se coordinarán todos los cambios relacionados. La clase solo necesita un método Save y una propiedad para cada repositorio. Cada propiedad del repositorio devuelve una instancia de repositorio que se ha creado mediante la misma instancia de contexto de base de datos que las demás.

En la carpeta DAL, cree un archivo de clase denominado UnitOfWork.cs y reemplace el código de plantilla con el código siguiente:

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

El código crea variables de clase para el contexto de la base de datos y para cada repositorio. Para la variable context, se crea una instancia de un nuevo contexto:

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

Cada propiedad del repositorio comprueba si el repositorio ya existe. Si no es así, crea una instancia del repositorio y le pasa la instancia de contexto. Como resultado, todos los repositorios comparten la misma instancia de contexto.

public GenericRepository<Department> DepartmentRepository
{
    get
    {

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

El método Save llama a SaveChanges en el contexto de la base de datos.

Como sucede con cualquier clase que crea instancias de un contexto de base de datos en una variable de clase, la clase UnitOfWork implementa IDisposable y elimina el contexto.

Cambio del controlador Course para usar la clase UnitOfWork y los repositorios

Reemplace el código que tiene actualmente en CourseController.cs por el siguiente:

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

Este código agrega una variable de clase para la clase UnitOfWork. (Si aquí usara interfaces, no inicializaría la variable; en su lugar, implementaría un patrón de dos constructores como hizo para el repositorio Student).

private UnitOfWork unitOfWork = new UnitOfWork();

En el resto de la clase, todas las referencias al contexto de base de datos se reemplazan por referencias al repositorio adecuado, mediante propiedades UnitOfWork para acceder al repositorio. El método Dispose elimina la instancia de UnitOfWork.

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

Ejecute el sitio y haga clic en la pestaña Courses.

Courses_Index_page

La página tiene el mismo aspecto y funciona igual que antes de los cambios, y las otras páginas Course también funcionan igual.

Resumen

Ahora ha implementado los patrones de repositorio y de unidad de trabajo. Ha usado expresiones lambda como parámetros de método en el repositorio genérico. Para información sobre cómo usar estas expresiones con un objeto IQueryable, vea Interfaz IQueryable(T) (System.Linq) en MSDN Library. En el siguiente tutorial aprenderá a controlar algunos escenarios avanzados.

En el mapa de contenido de acceso a datos de ASP.NET pueden encontrar vínculos a otros recursos de Entity Framework.