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.
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:
- O padrão do repositório no MSDN.
- Agile Entity Framework 4 Série de postagens no blog de Julie Lerman.
- Criando a conta em um aplicativo HTML5/jQuery relance no blog de Dan Wahlin.
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 .
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:
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 .
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 IStudentRepository
o . 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 GenericRepository
UnitOfWork
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 GetByID
métodos , Insert
e 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 .
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.