Implementando o repositório e a unidade de padrões de trabalho em um aplicativo MVC ASP.NET (9 de 10)

por Tom Dykstra

O aplicativo Web de exemplo da Contoso University demonstra como criar ASP.NET aplicativos MVC 4 usando o Entity Framework 5 Code First e o Visual Studio 2012. Para obter informações sobre a série de tutoriais, consulte o primeiro tutorial da série.

Observação

Se você tiver um problema, não poderá resolve, baixe o capítulo concluído e tente reproduzir o problema. Geralmente, você pode encontrar a solução para o problema comparando seu código com o código concluído. Para obter alguns erros comuns e como resolvê-los, consulte Erros e soluções alternativas.

No tutorial anterior, você usou a herança para reduzir o código redundante nas Student classes de entidade e Instructor . Neste tutorial, você verá algumas maneiras de usar o repositório e a unidade de padrões de trabalho para operações CRUD. Como no tutorial anterior, neste, você alterará a maneira como o código funciona com páginas que você já criou em vez de criar novas páginas.

O repositório e a unidade de padrões de trabalho

O repositório e a unidade de padrões de trabalho destinam-se a criar uma camada de abstração entre a camada de acesso a dados e a camada lógica de negócios de um aplicativo. A implementação desses padrões pode ajudar a isolar o aplicativo de alterações no armazenamento de dados e pode facilitar o teste de unidade automatizado ou TDD (desenvolvimento orientado por testes).

Neste tutorial, você implementará uma classe de repositório para cada tipo de entidade. Para o Student tipo de entidade, você criará uma interface de repositório e uma classe de repositório. Ao instanciar o repositório em seu controlador, você usará a interface para que o controlador aceite uma referência a qualquer objeto que implemente a interface do repositório. Quando o controlador é executado em um servidor Web, ele recebe um repositório que funciona com o Entity Framework. Quando o controlador é executado em uma classe de teste de unidade, ele recebe um repositório que funciona com dados armazenados de uma maneira que você pode manipular facilmente para teste, como uma coleção na memória.

Posteriormente, no tutorial, você usará vários repositórios e uma unidade de classe corporativa para os Course tipos de entidade e Department no Course controlador. A unidade da classe corporativa coordena o trabalho de vários repositórios criando uma única classe de contexto de banco de dados compartilhada por todos eles. Se você quisesse ser capaz de executar testes de unidade automatizados, criaria e usaria interfaces para essas classes da mesma maneira que fez para o Student repositório. No entanto, para manter o tutorial simples, você criará e usará essas classes sem interfaces.

A ilustração a seguir mostra uma maneira de conceituar as relações entre o controlador e as classes de contexto em comparação com não usar o repositório ou a unidade do padrão de trabalho.

Repository_pattern_diagram

Você não criará testes de unidade nesta série de tutoriais. Para obter uma introdução ao TDD com um aplicativo MVC que usa o padrão de repositório, consulte Passo a passo: usando TDD com ASP.NET MVC. Para obter mais informações sobre o padrão do repositório, consulte os seguintes recursos:

Observação

Há muitas maneiras de implementar o repositório e a unidade de padrões de trabalho. Você pode usar classes de repositório com ou sem uma unidade de classe corporativa. Você pode implementar um único repositório para todos os tipos de entidade ou um para cada tipo. Se você implementar um para cada tipo, poderá usar classes separadas, uma classe base genérica e classes derivadas ou uma classe base abstrata e classes derivadas. Você pode incluir lógica de negócios em seu repositório ou restringi-la à lógica de acesso a dados. Você também pode criar uma camada de abstração em sua classe de contexto de banco de dados usando interfaces IDbSet lá em vez de tipos DbSet para seus conjuntos de entidades. A abordagem para implementar uma camada de abstração mostrada neste tutorial é uma opção a ser considerada, não uma recomendação para todos os cenários e ambientes.

Criando a classe de repositório de alunos

Na pasta DAL , crie um arquivo de classe chamado IStudentRepository.cs e substitua o código existente pelo seguinte código:

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

Esse código declara um conjunto típico de métodos CRUD, incluindo dois métodos de leitura, um que retorna todas as Student entidades e outro que localiza uma única Student entidade por ID.

Na pasta DAL , crie um arquivo de classe chamado Arquivo StudentRepository.cs . Substitua o código existente pelo seguinte código, que implementa a IStudentRepository interface :

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

O contexto do banco de dados é definido em uma variável de classe e o construtor espera que o objeto de chamada passe em uma instância do contexto:

private SchoolContext context;

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

Você poderia criar uma instância de um novo contexto no repositório, mas, se você usasse vários repositórios em um controlador, cada um acabaria com um contexto separado. Posteriormente, você usará vários repositórios no Course controlador e verá como uma unidade da classe corporativa pode garantir que todos os repositórios usem o mesmo contexto.

O repositório implementa IDisposable e descarta o contexto do banco de dados como você viu anteriormente no controlador e seus métodos CRUD fazem chamadas para o contexto do banco de dados da mesma maneira que você viu anteriormente.

Alterar o controlador de alunos para usar o repositório

Em StudentController.cs, substitua o código atualmente na classe pelo código a seguir. As alterações são realçadas.

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

O controlador agora declara uma variável de classe para um objeto que implementa a IStudentRepository interface em vez da classe de contexto:

private IStudentRepository studentRepository;

O construtor padrão (sem parâmetros) cria uma nova instância de contexto e um construtor opcional permite que o chamador passe em uma instância de contexto.

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

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

(Se você estivesse usando injeção de dependência ou DI, não precisaria do construtor padrão porque o software DI garantiria que o objeto de repositório correto sempre seria fornecido.)

Nos métodos CRUD, o repositório agora é chamado em vez do 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();

E o Dispose método agora descarta o repositório em vez do contexto:

studentRepository.Dispose();

Execute o site e clique na guia Alunos .

Students_Index_page

A página parece e funciona da mesma forma que antes de você alterar o código para usar o repositório e as outras páginas do Student também funcionam da mesma forma. No entanto, há uma diferença importante na maneira como o Index método do controlador faz a filtragem e a ordenação. A versão original desse método continha o seguinte código:

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

O método atualizado Index contém o seguinte código:

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

Somente o código realçado foi alterado.

Na versão original do código, students é digitado como um IQueryable objeto . A consulta não é enviada ao banco de dados até ser convertida em uma coleção usando um método como ToList, que não ocorre até que a exibição Índice acesse o modelo de aluno. O Where método no código original acima torna-se uma WHERE cláusula na consulta SQL que é enviada ao banco de dados. Isso, por sua vez, significa que apenas as entidades selecionadas são retornadas pelo banco de dados. No entanto, como resultado da alteração context.Students para studentRepository.GetStudents(), a students variável após essa instrução é uma coleção IEnumerable que inclui todos os alunos no banco de dados. O resultado final da aplicação do Where método é o mesmo, mas agora o trabalho é feito na memória no servidor Web e não pelo banco de dados. Para consultas que retornam grandes volumes de dados, isso pode ser ineficiente.

Dica

IQueryable x IEnumerable

Depois de implementar o repositório conforme mostrado aqui, mesmo que você insira algo na caixa Pesquisar, a consulta enviada para SQL Server retorna todas as linhas student porque ela não inclui seus critérios de pesquisa:

Captura de tela do código que mostra o novo repositório de alunos implementado e realçado.

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'

Essa consulta retorna todos os dados do aluno porque o repositório executou a consulta sem saber sobre os critérios de pesquisa. O processo de classificação, aplicação de critérios de pesquisa e a seleção de um subconjunto dos dados para paginação (mostrando apenas três linhas nesse caso) é feito na memória posteriormente quando o ToPagedList método é chamado na IEnumerable coleção.

Na versão anterior do código (antes de implementar o repositório), a consulta não será enviada ao banco de dados até que você aplique os critérios de pesquisa, quando ToPagedList for chamado no IQueryable objeto .

Captura de tela que mostra o código do Controlador de Alunos. Uma linha de código de cadeia de caracteres de pesquisa e a linha de código Para Lista Paginada são realçadas.

Quando ToPagedList é chamado em um IQueryable objeto, a consulta enviada para SQL Server especifica a cadeia de caracteres de pesquisa e, como resultado, apenas as linhas que atendem aos critérios de pesquisa são retornadas e nenhuma filtragem precisa ser feita na memória.

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'

(O tutorial a seguir explica como examinar as consultas enviadas ao SQL Server.)

A seção a seguir mostra como implementar métodos de repositório que permitem especificar que esse trabalho deve ser feito pelo banco de dados.

Agora você criou uma camada de abstração entre o controlador e o contexto de banco de dados do Entity Framework. Se você fosse executar testes de unidade automatizados com este aplicativo, poderia criar uma classe de repositório alternativa em um projeto de teste de unidade que implementa IStudentRepositoryo . Em vez de chamar o contexto para ler e gravar dados, essa classe de repositório simulada pode manipular coleções na memória para testar as funções do controlador.

Implementar um repositório genérico e uma unidade de classe corporativa

A criação de uma classe de repositório para cada tipo de entidade pode resultar em muitos códigos redundantes e isso pode resultar em atualizações parciais. Por exemplo, suponha que você precise atualizar dois tipos de entidade diferentes como parte da mesma transação. Se cada uma usar uma instância de contexto de banco de dados separada, uma poderá ter êxito e a outra poderá falhar. Uma maneira de minimizar o código redundante é usar um repositório genérico e uma maneira de garantir que todos os repositórios usem o mesmo contexto de banco de dados (e, portanto, coordenar todas as atualizações) é usar uma unidade de classe corporativa.

Nesta seção do tutorial, você criará uma classe e uma GenericRepositoryUnitOfWork classe e as usará no Course controlador para acessar os Department conjuntos de entidades e Course . Conforme explicado anteriormente, para manter essa parte do tutorial simples, você não está criando interfaces para essas classes. Mas se você fosse usá-los para facilitar o TDD, normalmente os implementaria com interfaces da mesma maneira que fez com o Student repositório.

Criar um repositório genérico

Na pasta DAL , crie GenericRepository.cs e substitua o código existente pelo seguinte código:

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

As variáveis de classe são declaradas para o contexto do banco de dados e para o conjunto de entidades para o qual o repositório é instanciado:

internal SchoolContext context;
internal DbSet dbSet;

O construtor aceita uma instância de contexto de banco de dados e inicializa a variável de conjunto de entidades:

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

O Get método usa expressões lambda para permitir que o código de chamada especifique uma condição de filtro e uma coluna para ordenar os resultados e um parâmetro de cadeia de caracteres permite que o chamador forneça uma lista delimitada por vírgulas de propriedades de navegação para carregamento ansioso:

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

O código Expression<Func<TEntity, bool>> filter significa que o chamador fornecerá uma expressão lambda com base no TEntity tipo e essa expressão retornará um valor booliano. Por exemplo, se o repositório for instanciado para o Student tipo de entidade, o código no método de chamada poderá especificar student => student.LastName == "Smith" para o filter parâmetro .

O código Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy também significa que o chamador fornecerá uma expressão lambda. Mas, nesse caso, a entrada para a expressão é um IQueryable objeto para o TEntity tipo. A expressão retornará uma versão ordenada desse IQueryable objeto. Por exemplo, se o repositório for instanciado para o Student tipo de entidade, o código no método de chamada poderá especificar q => q.OrderBy(s => s.LastName) para o orderBy parâmetro .

O código no Get método cria um IQueryable objeto e aplica a expressão de filtro se houver um:

IQueryable<TEntity> query = dbSet;

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

Em seguida, ele aplica as expressões de carregamento ansioso depois de analisar a lista delimitada por vírgulas:

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

Por fim, ele aplica a orderBy expressão se houver um e retorna os resultados; caso contrário, retorna os resultados da consulta não ordenada:

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

Ao chamar o Get método , você pode fazer filtragem e classificação na IEnumerable coleção retornada pelo método em vez de fornecer parâmetros para essas funções. Mas o trabalho de classificação e filtragem seria feito na memória no servidor Web. Usando esses parâmetros, você garante que o trabalho seja feito pelo banco de dados em vez do servidor Web. Uma alternativa é criar classes derivadas para tipos de entidade específicos e adicionar métodos especializados Get , como GetStudentsInNameOrder ou GetStudentsByName. No entanto, em um aplicativo complexo, isso pode resultar em um grande número dessas classes derivadas e métodos especializados, o que poderia ser mais trabalho para manter.

O código nos GetByIDmétodos , Inserte Update é semelhante ao que você viu no repositório não genérico. (Você não está fornecendo um parâmetro de carregamento ansioso na GetByID assinatura, porque não pode fazer carregamentos ansiosos com o Find método .)

Duas sobrecargas são fornecidas para o Delete método :

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

Uma delas permite que você passe apenas a ID da entidade a ser excluída e uma delas usa uma instância de entidade. Como você viu no tutorial Manipulando simultaneidade , para tratamento de simultaneidade, você precisa de um Delete método que usa uma instância de entidade que inclui o valor original de uma propriedade de acompanhamento.

Esse repositório genérico tratará os requisitos crud típicos. Quando um tipo de entidade específico tem requisitos especiais, como filtragem ou ordenação mais complexas, você pode criar uma classe derivada que tenha métodos adicionais para esse tipo.

Criando a unidade de classe corporativa

A unidade da classe corporativa serve a uma finalidade: para garantir que, ao usar vários repositórios, eles compartilhem um único contexto de banco de dados. Dessa forma, quando uma unidade de trabalho for concluída, você poderá chamar o SaveChanges método nessa instância do contexto e ter certeza de que todas as alterações relacionadas serão coordenadas. Tudo o que a classe precisa é de um Save método e uma propriedade para cada repositório. Cada propriedade do repositório retorna uma instância do repositório que foi instanciada usando a mesma instância de contexto de banco de dados que as outras instâncias do repositório.

Na pasta DAL , crie um arquivo de classe chamado UnitOfWork.cs e substitua o código de modelo pelo seguinte código:

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

O código cria variáveis de classe para o contexto do banco de dados e cada repositório. Para a context variável, um novo contexto é instanciado:

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

Cada propriedade do repositório verifica se o repositório já existe. Caso contrário, ele instancia o repositório, passando a instância de contexto. Como resultado, todos os repositórios compartilham a mesma instância de contexto.

public GenericRepository<Department> DepartmentRepository
{
    get
    {

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

O Save método chama SaveChanges no contexto do banco de dados.

Como qualquer classe que instancia um contexto de banco de dados em uma variável de classe, a UnitOfWork classe implementa IDisposable e descarta o contexto.

Alterando o controlador de curso para usar a classe UnitOfWork e repositórios

Substitua o código que você tem atualmente em CourseController.cs pelo seguinte código:

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

Esse código adiciona uma variável de classe para a UnitOfWork classe . (Se você estivesse usando interfaces aqui, não inicializaria a variável aqui; em vez disso, implementaria um padrão de dois construtores exatamente como fez para o Student repositório.)

private UnitOfWork unitOfWork = new UnitOfWork();

No restante da classe, todas as referências ao contexto do banco de dados são substituídas por referências ao repositório apropriado, usando UnitOfWork propriedades para acessar o repositório. O Dispose método descarta a UnitOfWork instância.

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

Execute o site e clique na guia Cursos .

Courses_Index_page

A página parece e funciona da mesma forma que antes das alterações, e as outras páginas do Curso também funcionam da mesma forma.

Resumo

Agora você implementou o repositório e a unidade de padrões de trabalho. Você usou expressões lambda como parâmetros de método no repositório genérico. Para obter mais informações sobre como usar essas expressões com um IQueryable objeto, consulte Interface IQueryable(T) (System.Linq) no Biblioteca MSDN. No próximo tutorial, você aprenderá a lidar com alguns cenários avançados.

Links para outros recursos do Entity Framework podem ser encontrados no mapa de conteúdo de acesso a dados do ASP.NET.