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

Том Дайкстра

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

Примечание

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

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

Шаблоны репозитория и единицы работы

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

В этом руководстве вы реализуете класс репозитория для каждого типа сущности. Для типа сущности 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, конструктор по умолчанию не потребуется, так как программное обеспечение внедрения зависимостей гарантирует, что правильный объект репозитория всегда будет предоставляться.)

В методах 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.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. Вместо вызова контекста для чтения и записи данных этот макетный класс репозитория может управлять коллекциями в памяти для тестирования функций контроллера.

Реализация универсального репозитория и класса Unit of Work

Создание класса репозитория для каждого типа сущности может привести к большим объемам избыточного кода, а также к частичным обновлениям. Например, предположим, что необходимо обновить два разных типа сущностей в рамках одной транзакции. Если каждый из них использует отдельный экземпляр контекста базы данных, один из них может завершиться успешно, а другой — сбоем. Один из способов свести к минимуму избыточный код — использовать универсальный репозиторий, а один из способов убедиться, что все репозитории используют один и тот же контекст базы данных (и, таким образом, координировать все обновления), — использовать единицу рабочего класса.

В этом разделе учебника вы создадите 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. Если к определенному типу сущности предъявляют особые требования, такие как более сложная фильтрация или упорядочение, можно создать производный класс с дополнительными методами для этого типа.

Создание класса unit of Work

Единица рабочего класса служит одной цели: убедиться, что при использовании нескольких репозиториев они совместно используют один контекст базы данных. Таким образом, после завершения единицы работы можно вызвать 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 схеме содержимого доступа к данным.