Реализация репозитория и шаблонов единиц работы в приложении MVC ASP.NET (9 из 10)
Пример веб-приложения Университета Contoso демонстрирует создание ASP.NET приложений MVC 4 с помощью Entity Framework 5 Code First и Visual Studio 2012. Сведения о серии руководств см. в первом руководстве серии.
Примечание
Если вам не удается устранить проблему, скачайте завершенную главу и попытайтесь воспроизвести проблему. Как правило, решение проблемы можно найти, сравнивая код с готовым кодом. Некоторые распространенные ошибки и способы их устранения см. в разделе "Ошибки и обходные решения".
В предыдущем руководстве вы использовали наследование для уменьшения избыточного Student
кода в классах сущностей и Instructor
объектов. В этом руководстве вы узнаете, как использовать репозиторий и единицы рабочих шаблонов для операций CRUD. Как и в предыдущем руководстве, в этом руководстве вы измените способ работы кода с уже созданными страницами, а не с новыми страницами.
Репозиторий и шаблоны единиц работы
Репозиторий и единица рабочих шаблонов предназначены для создания уровня абстракции между уровнем доступа к данным и уровнем бизнес-логики приложения. Реализация таких шаблонов позволяет изолировать приложение от изменений в хранилище данных и упрощает автоматическое модульное тестирование или разработку на основе тестирования.
В этом руководстве описано, как реализовать класс репозитория для каждого типа сущности. Для типа сущности Student
вы создадите интерфейс репозитория и класс репозитория. При создании экземпляра репозитория в контроллере вы будете использовать интерфейс, чтобы контроллер принял ссылку на любой объект, реализующий интерфейс репозитория. Когда контроллер выполняется под веб-сервером, он получает репозиторий, который работает с Entity Framework. Когда контроллер выполняется в классе модульного теста, он получает репозиторий, который работает с данными, хранящимися таким образом, чтобы можно было легко управлять тестированием, например с коллекцией в памяти.
Далее в этом руководстве вы будете использовать несколько репозиториев и единицу рабочего класса для Course
Department
типов сущностей в контроллере Course
. Единица рабочего класса координирует работу нескольких репозиториев путем создания одного класса контекста базы данных, совместно используемого всеми из них. Если вы хотите иметь возможность выполнять автоматическое модульное тестирование, создайте и используйте интерфейсы для этих классов так же, как и для репозитория Student
. Однако для простоты учебника вы создадите и используете эти классы без интерфейсов.
На следующем рисунке показан один из способов концептуальной концепции связей между контроллером и классами контекста по сравнению с тем, что репозиторий или единица работы вообще не используются.
Модульные тесты не создаются в этой серии руководств. Общие сведения о TDD с приложением MVC, использующим шаблон репозитория, см. в пошаговом руководстве. Использование TDD с ASP.NET MVC. Дополнительные сведения о шаблоне репозитория см. в следующих ресурсах:
- Шаблон репозитория на сайте MSDN.
- Серия записей репозитория Agile Entity Framework 4 в блоге Джули Лермана.
- Создание учетной записи на краткий html5/jQuery Application на блоге Дэна Вахлина.
Примечание
Существует множество способов реализации репозитория и единиц работы. Классы репозитория можно использовать с единицей рабочего класса или без нее. Вы можете реализовать один репозиторий для всех типов сущностей или один для каждого типа. При реализации одного для каждого типа можно использовать отдельные классы, универсальный базовый класс и производные классы, абстрактный базовый класс и производные классы. Бизнес-логику можно включить в репозиторий или ограничить ее логикой доступа к данным. Можно также создать уровень абстракции в классе контекста базы данных с помощью интерфейсов IDbSet вместо типов DbSet для наборов сущностей. Подход к реализации уровня абстракции, показанного в этом руководстве, является одним из вариантов, которые следует рассмотреть, а не рекомендации для всех сценариев и сред.
Создание класса репозитория учащихся
В папке DAL создайте файл класса с именем IStudentRepository.cs и замените существующий код следующим кодом:
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();
}
}
Этот код объявляет типичный набор методов CRUD, включая два метода чтения — один, который возвращает все Student
сущности, и тот, который находит одну Student
сущность по идентификатору.
В папке DAL создайте файл класса StudentRepository.cs . Замените существующий код следующим кодом, который реализует 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);
}
}
}
Контекст базы данных определяется в переменной класса, и конструктор ожидает, что вызывающий объект передается в экземпляр контекста:
private SchoolContext context;
public StudentRepository(SchoolContext context)
{
this.context = context;
}
Вы можете создать новый контекст в репозитории, но если вы использовали несколько репозиториев в одном контроллере, каждый из них будет иметь отдельный контекст. Позже вы будете использовать несколько репозиториев в контроллере Course
, и вы увидите, как единица рабочего класса может гарантировать, что все репозитории используют один и тот же контекст.
Репозиторий реализует интерфейс IDisposable и удаляет контекст базы данных, как вы видели ранее в контроллере, и его методы CRUD выполняют вызовы контекста базы данных так же, как вы видели ранее.
Изменение контроллера учащегося на использование репозитория
В Файле StudentController.cs замените код в данный момент в классе следующим кодом. Изменения выделены.
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);
}
}
}
Теперь контроллер объявляет переменную класса для объекта, реализующего IStudentRepository
интерфейс, а не класс контекста:
private IStudentRepository studentRepository;
Конструктор по умолчанию (без параметров) создает новый экземпляр контекста, а необязательный конструктор позволяет вызывающей стороны передавать в экземпляр контекста.
public StudentController()
{
this.studentRepository = new StudentRepository(new SchoolContext());
}
public StudentController(IStudentRepository studentRepository)
{
this.studentRepository = studentRepository;
}
(Если вы использовали внедрение зависимостей или внедрения зависимостей, вам не потребуется конструктор по умолчанию, так как программное обеспечение внедрения зависимостей гарантирует, что правильный объект репозитория всегда будет предоставлен.)
В методах CRUD репозиторий теперь вызывается вместо контекста:
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();
Dispose
Теперь метод удаляет репозиторий вместо контекста:
studentRepository.Dispose();
Запустите сайт и перейдите на вкладку "Учащиеся ".
Страница выглядит и работает так же, как и перед изменением кода для использования репозитория, а другие страницы student также работают так же. Однако есть важное различие в том, как Index
метод контроллера выполняет фильтрацию и упорядочение. Исходная версия этого метода содержала следующий код:
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()));
}
Обновленный Index
метод содержит следующий код:
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()));
}
Изменен только выделенный код.
В исходной версии кода students
вводится как IQueryable
объект. Запрос не отправляется в базу данных, пока он не будет преобразован в коллекцию с помощью такого метода, как ToList
, который не будет выполняться до тех пор, пока представление индекса не будет обращаться к модели учащегося. Метод Where
в исходном коде выше становится предложением WHERE
в SQL-запросе, который отправляется в базу данных. Это, в свою очередь, означает, что база данных возвращает только выбранные сущности. Однако в результате изменения context.Students
studentRepository.GetStudents()
students
переменной после этого оператора является IEnumerable
коллекция, включающая всех учащихся в базу данных. Конечный результат применения Where
метода одинаков, но теперь работа выполняется в памяти на веб-сервере, а не в базе данных. Для запросов, возвращающих большие объемы данных, это может быть неэффективным.
Совет
IQueryable и IEnumerable
После реализации репозитория, как показано здесь, даже если вы введете что-то в поле поиска, запрос, отправленный в SQL Server возвращает все строки student, так как он не включает условия поиска:
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'
Этот запрос возвращает все данные учащегося, так как репозиторий выполнил запрос, не зная о критериях поиска. Процесс сортировки, применения условий поиска и выбора подмножества данных для разбиения на страницы (в этом случае отображается только 3 строки) выполняется в памяти позже при ToPagedList
вызове метода в IEnumerable
коллекции.
В предыдущей версии кода (перед реализацией репозитория) запрос не отправляется в базу данных до тех пор, пока вы не примените условия поиска при ToPagedList
вызове IQueryable
объекта.
При вызове ToPagedList для IQueryable
объекта запрос, отправляемый в SQL Server указывает строку поиска, а в результате возвращаются только строки, соответствующие условиям поиска, и фильтрация в памяти не требуется.
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'
(В следующем руководстве объясняется, как изучить запросы, отправленные в SQL Server.)
В следующем разделе показано, как реализовать методы репозитория, позволяющие указать, что эта работа должна выполняться базой данных.
Теперь вы создали уровень абстракции между контроллером и контекстом базы данных Entity Framework. Если вы собираетесь выполнить автоматическое модульное тестирование с помощью этого приложения, можно создать альтернативный класс репозитория в проекте модульного теста, который реализует IStudentRepository
. Вместо вызова контекста для чтения и записи данных этот класс репозитория макета может управлять коллекциями в памяти для тестирования функций контроллера.
Реализация универсального репозитория и единиц рабочего класса
Создание класса репозитория для каждого типа сущности может привести к созданию большого количества избыточного кода, что может привести к частичным обновлениям. Например, предположим, что необходимо обновить два разных типа сущностей в рамках одной транзакции. Если каждый из них использует отдельный экземпляр контекста базы данных, один из них может завершиться успешно, а другой может завершиться ошибкой. Один из способов свести к минимуму избыточный код — использовать универсальный репозиторий, и один из способов убедиться, что все репозитории используют один и тот же контекст базы данных (и, таким образом, координируют все обновления), — использовать единицу рабочего класса.
В этом разделе руководства вы создадите GenericRepository
класс и UnitOfWork
класс и используйте их в Course
контроллере для доступа как к наборам сущностей, так Department
и к наборам Course
сущностей. Как было сказано ранее, чтобы упростить эту часть учебника, вы не создаете интерфейсы для этих классов. Но если вы собираетесь использовать их для упрощения TDD, вы обычно реализуете их с интерфейсами так же, как и репозиторий Student
.
Создание универсального репозитория
В папке DAL создайте файл GenericRepository.cs и замените существующий код следующим кодом:
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;
}
}
}
Переменные класса объявляются для контекста базы данных и для набора сущностей, для которого создается экземпляр репозитория:
internal SchoolContext context;
internal DbSet dbSet;
Конструктор принимает экземпляр контекста базы данных и инициализирует переменную набора сущностей:
public GenericRepository(SchoolContext context)
{
this.context = context;
this.dbSet = context.Set<TEntity>();
}
Метод Get
использует лямбда-выражения, чтобы разрешить вызывающему коду указать условие фильтра и столбец для упорядочивания результатов, а строковый параметр позволяет вызывающему объекту предоставить список свойств навигации с разделителями-запятыми для безотложной загрузки:
public virtual IEnumerable<TEntity> Get(
Expression<Func<TEntity, bool>> filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
string includeProperties = "")
Код Expression<Func<TEntity, bool>> filter
означает, что вызывающий объект предоставит лямбда-выражение на TEntity
основе типа, и это выражение вернет логическое значение. Например, если репозиторий создается для Student
типа сущности, код в вызывающем методе может указать student => student.LastName == "Smith
"для filter
параметра.
Код Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy
также означает, что вызывающий объект предоставит лямбда-выражение. Но в этом случае входные данные выражения являются IQueryable
объектом для TEntity
типа. Выражение вернет упорядоченную версию этого IQueryable
объекта. Например, если репозиторий создается для Student
типа сущности, код в вызывающем методе может указать q => q.OrderBy(s => s.LastName)
для orderBy
параметра.
Код в методе Get
создает IQueryable
объект, а затем применяет выражение фильтра, если таковое имеется:
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);
}
Наконец, оно применяет orderBy
выражение, если он есть и возвращает результаты; в противном случае он возвращает результаты из неупорядоченного запроса:
if (orderBy != null)
{
return orderBy(query).ToList();
}
else
{
return query.ToList();
}
При вызове Get
метода можно выполнять фильтрацию и сортировку IEnumerable
по коллекции, возвращаемой методом, вместо предоставления параметров для этих функций. Но сортировка и фильтрация будут выполняться в памяти на веб-сервере. Используя эти параметры, вы гарантируете, что работа выполняется базой данных, а не веб-сервером. Альтернативой является создание производных классов для определенных типов сущностей и добавление специализированных Get
методов, таких как GetStudentsInNameOrder
или GetStudentsByName
. Однако в сложном приложении это может привести к большому количеству таких производных классов и специализированных методов, которые могут быть более сложными для поддержания.
Код в файле GetByID
, Insert
и Update
методы похожи на то, что вы видели в неуниверсационном репозитории. (Вы не предоставляете параметр безотложной загрузки в сигнатуре GetByID
, так как невозможно выполнить неотложную загрузку Find
с помощью метода.)
Для метода предоставляются две перегрузки 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);
}
Один из них позволяет передавать только идентификатор удаляемой сущности, а один принимает экземпляр сущности. Как вы видели в руководстве по обработке параллелизма , для обработки параллелизма требуется Delete
метод, который принимает экземпляр сущности, включающий исходное значение свойства отслеживания.
Этот универсальный репозиторий будет обрабатывать типичные требования CRUD. Если определенный тип сущности имеет особые требования, такие как более сложная фильтрация или упорядочение, можно создать производный класс, имеющий дополнительные методы для этого типа.
Создание единицы работы
Единица рабочего класса служит одной цели: чтобы убедиться, что при использовании нескольких репозиториев они совместно используют один контекст базы данных. Таким образом, после завершения единицы работы можно вызвать SaveChanges
метод для этого экземпляра контекста и быть уверенным, что все связанные изменения будут скоординированы. Все, что требуется классу, является методом и свойством Save
для каждого репозитория. Каждое свойство репозитория возвращает экземпляр репозитория, созданный с помощью того же экземпляра контекста базы данных, что и другие экземпляры репозитория.
В папке DAL создайте файл класса с именем UnitOfWork.cs и замените код шаблона следующим кодом:
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);
}
}
}
Код создает переменные класса для контекста базы данных и каждого репозитория. Для переменной context
создается новый контекст:
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;
}
}
Метод Save
вызывает SaveChanges
контекст базы данных.
Как и любой класс, создающий экземпляр контекста базы данных в переменной класса, UnitOfWork
класс реализует IDisposable
и удаляет контекст.
Изменение контроллера курса для использования класса UnitOfWork и репозиториев
Замените код, который вы используете в файле CourseController.cs , следующим кодом:
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);
}
}
}
Этот код добавляет переменную класса для UnitOfWork
класса. (Если вы использовали здесь интерфейсы, то не инициализируйте переменную здесь. Вместо этого можно реализовать шаблон двух конструкторов так же, как и для Student
репозитория.)
private UnitOfWork unitOfWork = new UnitOfWork();
В остальной части класса все ссылки на контекст базы данных заменяются ссылками на соответствующий репозиторий с помощью UnitOfWork
свойств для доступа к репозиторию. Метод Dispose
удаляет 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();
Запустите сайт и перейдите на вкладку "Курсы ".
Страница выглядит и работает так же, как и до внесения изменений, а другие страницы курсов также работают так же.
Сводка
Теперь вы реализовали репозиторий и единицу рабочих шаблонов. Лямбда-выражения использовались в качестве параметров метода в универсальном репозитории. Дополнительные сведения об использовании этих выражений IQueryable
с объектом см. в разделе "Интерфейс IQueryable(T) (System.Linq) в библиотека MSDN. В следующем руководстве вы узнаете, как обрабатывать некоторые сложные сценарии.
Ссылки на другие ресурсы Entity Framework можно найти на карте содержимого доступа к данным ASP.NET.