Реализация репозитория и шаблонов единиц работы в приложении MVC ASP.NET (9 из 10)

Том Дайкстра

Пример веб-приложения Contoso University демонстрирует создание ASP.NET приложений MVC 4 с помощью Entity Framework 5 Code First и Visual Studio 2012. Сведения о серии учебников см. в первом руководстве в серии.

Замечание

Если вы не сможете устранить проблему, скачайте завершенную главу и попытайтесь воспроизвести проблему. Как правило, решение проблемы можно найти, сравнивая код с завершенным кодом. Некоторые распространенные ошибки и способы их устранения см. в статье об ошибках и обходных решениях.

В предыдущем руководстве вы использовали наследование для сокращения избыточного кода в классах сущностей Student и Instructor. В этом руководстве вы узнаете, как использовать репозиторий и единицы рабочих шаблонов для операций CRUD. Как и в предыдущем руководстве, в этом руководстве вы измените способ работы кода с уже созданными страницами, а не с новыми страницами.

Репозиторий и единицы рабочих шаблонов

Репозиторий и единица рабочих шаблонов предназначены для создания слоя абстракции между уровнем доступа к данным и уровнем бизнес-логики приложения. Реализация этих шаблонов может помочь изолировать приложение от изменений в хранилище данных и упростить автоматизированное модульное тестирование или разработку на основе тестов (TDD).

В этом руководстве описано, как реализовать класс репозитория для каждого типа сущности. Student Для типа сущности вы создадите интерфейс репозитория и класс репозитория. При создании экземпляра репозитория в контроллере вы будете использовать интерфейс, чтобы контроллер принял ссылку на любой объект, реализующий интерфейс репозитория. Когда контроллер работает под веб-сервером, он получает репозиторий, который работает с Entity Framework. Когда контроллер выполняется в классе модульного теста, он получает репозиторий, который работает с данными, хранящимися таким образом, чтобы можно было легко управлять тестированием, например коллекцией в памяти.

Далее в руководстве вы будете использовать несколько репозиториев и единицу работы для типов сущностей Course и Department в контроллере Course. Единица рабочего класса координирует работу нескольких репозиториев, создав один класс контекста базы данных, общий для всех из них. Если вы хотите иметь возможность выполнять автоматизированное модульное тестирование, вы создадите и используйте интерфейсы для этих классов так же, как и для Student репозитория. Однако для простоты учебника вы создадите и используйте эти классы без интерфейсов.

На следующем рисунке показан один из способов концептуальной концепции связей между контроллером и классами контекста по сравнению с тем, чтобы не использовать репозиторий или единицу рабочего шаблона вообще.

Repository_pattern_diagram

Вы не создадите модульные тесты в этой серии руководств. Общие сведения о TDD с приложением MVC, использующим шаблон репозитория, см. в пошаговом руководстве. Использование TDD с ASP.NET MVC. Дополнительные сведения о шаблоне репозитория см. в следующих ресурсах:

Замечание

Существует множество способов реализации репозитория и единицы рабочих шаблонов. Классы репозитория можно использовать с единицей рабочего класса или без нее. Вы можете реализовать один репозиторий для всех типов сущностей или один для каждого типа. При реализации одного для каждого типа можно использовать отдельные классы, универсальный базовый класс и производные классы, абстрактный базовый класс и производные классы. Бизнес-логику можно включить в репозиторий или ограничить ее логикой доступа к данным. Кроме того, можно создать слой абстракции в класс контекста базы данных с помощью интерфейсов 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;
}

(Если вы использовали внедрение зависимостей или DI, вам не потребуется конструктор по умолчанию, так как программное обеспечение DI гарантирует, что правильный объект репозитория всегда будет предоставлен.)

В методах 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();

Запустите сайт и перейдите на вкладку "Учащиеся ".

Students_Index_page

Страница выглядит и работает так же, как и перед изменением кода для использования репозитория, а другие страницы учащихся также работают так же. Однако есть важное различие в том, как 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, что происходит только тогда, когда представление Index обращается к модели учащихся. Метод Where в исходном коде выше становится предложением WHERE в SQL-запросе, который отправляется в базу данных. Это означает, что только выбранные сущности возвращаются базой данных. Однако, в результате изменения context.Students на studentRepository.GetStudents(), переменная students после этого оператора является коллекцией, включающей всех студентов в базе данных. Конечный результат применения Where метода совпадает, но теперь работа выполняется в памяти на веб-сервере, а не в базе данных. Для запросов, возвращающих большие объемы данных, это может быть неэффективно.

Подсказка

IQueryable vs. IEnumerable

После внедрения репозитория, как показано здесь, даже если вы вводите что-то в поле поиска, запрос, отправленный в SQL Server, возвращает все строки учащихся, так как он не включает ваши критерии поиска:

Снимок экрана с кодом, демонстрирующий новый репозиторий студентов, который был реализован и выделен.

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

Запустите сайт и перейдите на вкладку "Курсы ".

Courses_Index_page

Страница выглядит и работает так же, как и перед изменениями, а другие страницы курсов также работают так же.

Сводка

Теперь вы реализовали оба паттерна: репозитория и единицы работы. Вы использовали лямбда-выражения в качестве параметров метода в универсальном репозитории. Дополнительные сведения о том, как использовать эти выражения с объектом , см. в разделе «Интерфейс IQueryable(T) (System.Linq)» в библиотеке MSDN. В следующем руководстве вы узнаете, как обрабатывать некоторые сложные сценарии.

Ссылки на другие ресурсы Entity Framework можно найти на карте содержимого доступа к данным ASP.NET.