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

Том Дайкстра

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

Примечание

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

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

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

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

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

Далее в этом руководстве вы будете использовать несколько репозиториев и единицу рабочего класса для CourseDepartment типов сущностей в контроллере 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;
}

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

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

Страница выглядит и работает так же, как и перед изменением кода для использования репозитория, а другие страницы 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.StudentsstudentRepository.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();

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

Courses_Index_page

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

Сводка

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

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